NET Core, JWT+Auth2.0 implements SSO, with complete source code (.NET6)

1, Introduction

SingleSignOn (SSO)

It means that in multiple application systems, you can access other mutually trusted application systems only by logging in once.

JWT

The Json Web Token, which is not described in detail here, is simply an authentication mechanism.

Auth2.0

Auth2.0 is an authentication process. There are four methods in total. The most commonly used authorization code method is used here. The process is:

1. System A first obtains an authorization code from the certification center.

2. System A obtains token and refresh through authorization code_ Token, expiry_time, scope.

Token: the token carried by system A when it obtains A resource request from the authenticator.

refresh_token: the valid period of a token is relatively short. It is used to refresh the token.

expiry_time: token expiration time.

Scope: resource domain, which refers to the resource permission of system A. for example, scope:["userinfo"], system a only has the permission to obtain user information. For example, in normal times, the website can only be authorized to access the basic information of wechat users.

The SSO here is the company's own system, which is used to obtain user information, so this is empty. The scope is required to judge the resource permissions only when the third party needs to access our login.

2, Achieve goals

1. One login, all login

The flow chart is:

 

 

 

 

 

1. The browser accesses system A and finds that system A is not logged in. It jumps to the unified login Center (SSO) and brings the callback address of system A,

Address: https://sso.com/SSO/Login?redirectUrl=https://web1.com/Account/LoginRedirect&clientId=web1 , enter the user name and password, log in successfully, generate the authorization code, create A global session (cookie, redis), and jump back to the system A address with the authorization code: https://web1.com/Account/LoginRedirect?AuthCode=xxxxxxxx . Then, the callback address of system A uses this authcode to call SSO to obtain the token, obtain the token, create A local session (cookie, redis), and jump to https://web1.com . In this way, system A completes the login.

2. The browser accesses system B and finds that system B is not logged in. It jumps to the unified login Center (SSO) and brings the callback address of system B,

Address: https://sso.com/SSO/Login?redirectUrl=https://web2.com/Account/LoginRedirect&clientId=web2 SSO has a global session certificate to prove that it has logged in. It can directly use the global session code to obtain the authorization code of system B,

Jump back to system B with authorization code https://web2.com/Account/LoginRedirect?AuthCode=xxxxxxxx Then, the callback address of system B uses this authcode to call SSO to obtain the token. After obtaining the token, a local session (cookie, redis) is created, and then jump to https://web2.com . There is no need to enter the user name and password in the whole process. These jumps are basically insensitive, so B will log in automatically.

 

Why do you need multiple authorization codes without directly jumping back to system A and B with A token? Because the parameters on the address are easy to be intercepted, the token may be intercepted, which is very unsafe

In addition, for security, the authorization code can only be destroyed once. The token s of system A and system B are independent and cannot be accessed each other.

2. One exit, all exit

The flow chart is:

 

 

A system exits, deletes its own session, and then jumps to the exit login address of SSO: https://sso.com/SSO/Logout?redirectUrl=https://web1.com/Account/LoginRedirect&clientId=web1 , SSO deletes the global session, and then the calling interface deletes the system that has obtained the token, and then jumps to the login page, https://sso.com/SSO/Login?redirectUrl=https://web1.com/Account/LoginRedirect&clientId=web1 In this way, one exit and all exits are realized.

3. Double token mechanism

That is, with a refresh token, why refresh the token? Because token based authentication and authorization has inherent defects

The token takes a long time to set, and the token is leaked. Replay the attack.

The token setting is too short. You always have to log in. There are still many problems, because the nature of token determines that most of them cannot be solved.

 

Therefore, a double token mechanism is required. SSO returns a token and a refreshToken. The token is used for authentication, and the refreshToken refreshes the token,

For example, the valid period of a token is 10 minutes and the valid period of a refreshToken is 2 days. In this way, even if the token is leaked, it will expire in 10 minutes at most. The impact is not so great. The system refreshes the token every 9 minutes,

In this way, the system can make the token slip expire and avoid frequent re login.

III. function realization and core code

1. One login, all login

Build three projects, SSO project, web1 project and web2 project.

The process here is: web1 jumps to sso, enters the user name, logs in successfully, obtains the code, writes the session to the SSO cookie, and then jumps back to get the token according to the code and sso, and logs in successfully;

Then visit web2 and jump to SSO. SSO has logged in, and automatically gets the code. Jump back to web2 and get the token according to the code.

The key to realizing one login and everywhere login is SSO cookie s.

Then there is a core problem. If all the generated tokens are valid for 24 hours, web1 login succeeds and the obtained tokens are valid for 24 hours,

After 12 hours, I visit web2, and web2 also gets a 24-hour token. After 12 hours, web1's login expires, but web2 has not expired,

In this way, web2 is in login status, but web1 is not in login status and needs to log in again, which violates the concept of "one login everywhere".

Therefore, the token obtained later can only have the same expiration time as the token logged in for the first time. How to do this is to cache the expiration time when SSO logs in for the first time, and then according to the code obtained from the SSO session,

The expiration time of the changed token is the same as the first time.

SSO project

SSO project configuration file appsettings Add web1 and web2 information to JSON to verify the source and generate the jwt token of the corresponding project. The actual project should be saved to the database.

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "AppSetting": {
    "appHSSettings": [
      {
        "domain": "https://localhost:7001",
        "clientId": "web1",
        "clientSecret": "Nu4Ohg8mfpPnNxnXu53W4g0yWLqF0mX2"
      },
      {
        "domain": "https://localhost:7002",
        "clientId": "web2",
        "clientSecret": "pQeP5X9wejpFfQGgSjyWB8iFdLDGHEV8"
      }

    ]
  }
 
}

Domain: the domain name of the access system, which can be used to verify whether the request source is legal.

clientId: access system ID. when a token is requested, it is passed in to identify which system it is.

clientSecret: access system key, used to generate symmetric encrypted JWT.

Create an IJWTService to define the methods required for JWT generation

 /// <summary>
    /// JWT Service interface
    /// </summary>
    public interface IJWTService
    {
        /// <summary>
        /// Obtain authorization code
        /// </summary>
        /// <param name="userName"></param>
        /// <param name="password"></param>
        /// <returns></returns>
        /// <exception cref="NotImplementedException"></exception>
         ResponseModel<string> GetCode(string clientId, string userName, string password);
        /// <summary>
        /// Based on session Code Obtain authorization code
        /// </summary>
        /// <param name="clientId"></param>
        /// <param name="sessionCode"></param>
        /// <returns></returns>
        ResponseModel<string> GetCodeBySessionCode(string clientId, string sessionCode);

        /// <summary>
        /// Obtain according to authorization code Token+RefreshToken
        /// </summary>
        /// <param name="authCode"></param>
        /// <returns>Token+RefreshToken</returns>
        ResponseModel<GetTokenDTO> GetTokenWithRefresh(string authCode);

        /// <summary>
        /// according to RefreshToken Refresh Token
        /// </summary>
        /// <param name="refreshToken"></param>
        /// <param name="clientId"></param>
        /// <returns></returns>
        string GetTokenByRefresh(string refreshToken, string clientId);
    }

Build an abstract class JWTBaseService with template method to realize detailed logic

 /// <summary>
    /// jwt service
    /// </summary>
    public abstract class JWTBaseService : IJWTService
    {
        protected readonly IOptions<AppSettingOptions> _appSettingOptions;
        protected readonly Cachelper _cachelper;
        public JWTBaseService(IOptions<AppSettingOptions> appSettingOptions, Cachelper cachelper)
        {
            _appSettingOptions = appSettingOptions;
            _cachelper = cachelper;
        }

        /// <summary>
        /// Obtain authorization code
        /// </summary>
        /// <param name="userName"></param>
        /// <param name="password"></param>
        /// <returns></returns>
        /// <exception cref="NotImplementedException"></exception>
        public ResponseModel<string> GetCode(string clientId, string userName, string password)
        {
            ResponseModel<string> result = new ResponseModel<string>();

            string code = string.Empty;
            AppHSSetting appHSSetting = _appSettingOptions.Value.appHSSettings.Where(s => s.clientId == clientId).FirstOrDefault();
            if (appHSSetting == null)
            {
                result.SetFail("App does not exist");
                return result;
            }
            //Real project query database comparison
            if (!(userName == "admin" && password == "123456"))
            {
                result.SetFail("Username or password incorrect ");
                return result;
            }

            //User information
            CurrentUserModel currentUserModel = new CurrentUserModel
            {
                id = 101,
                account = "admin",
                name = "wolf",
                mobile = "13800138000",
                role = "SuperAdmin"
            };

            //Generate authorization code
            code = Guid.NewGuid().ToString().Replace("-", "").ToUpper();
            string key = $"AuthCode:{code}";
            string appCachekey = $"AuthCodeClientId:{code}";
            //Cache authorization code
            _cachelper.StringSet<CurrentUserModel>(key, currentUserModel, TimeSpan.FromMinutes(10));
            //Which application is the cache authorization code
            _cachelper.StringSet<string>(appCachekey, appHSSetting.clientId, TimeSpan.FromMinutes(10));
            //Create global session
            string sessionCode = $"SessionCode:{code}";
            SessionCodeUser sessionCodeUser = new SessionCodeUser
            {
                expiresTime = DateTime.Now.AddHours(1),
                currentUser = currentUserModel
            };
            _cachelper.StringSet<CurrentUserModel>(sessionCode, currentUserModel, TimeSpan.FromDays(1));
            //Global session expiration time
            string sessionExpiryKey = $"SessionExpiryKey:{code}";
            DateTime sessionExpirTime = DateTime.Now.AddDays(1);
            _cachelper.StringSet<DateTime>(sessionExpiryKey, sessionExpirTime, TimeSpan.FromDays(1));
            Console.WriteLine($"Login successful, global session code:{code}");
            //Cache authorization code fetching token Maximum effective time
            _cachelper.StringSet<DateTime>($"AuthCodeSessionTime:{code}", sessionExpirTime, TimeSpan.FromDays(1));

            result.SetSuccess(code);
            return result;
        }
        /// <summary>
        /// Based on session code Obtain authorization code
        /// </summary>
        /// <param name="clientId"></param>
        /// <param name="sessionCode"></param>
        /// <returns></returns>
        public ResponseModel<string> GetCodeBySessionCode(string clientId, string sessionCode)
        {
            ResponseModel<string> result = new ResponseModel<string>();
            string code = string.Empty;
            AppHSSetting appHSSetting = _appSettingOptions.Value.appHSSettings.Where(s => s.clientId == clientId).FirstOrDefault();
            if (appHSSetting == null)
            {
                result.SetFail("App does not exist");
                return result;
            }
            string codeKey = $"SessionCode:{sessionCode}";
            CurrentUserModel currentUserModel = _cachelper.StringGet<CurrentUserModel>(codeKey);
            if (currentUserModel == null)
            {
                return result.SetFail("Session does not exist or has expired", string.Empty);
            }

            //Generate authorization code
            code = Guid.NewGuid().ToString().Replace("-", "").ToUpper();
            string key = $"AuthCode:{code}";
            string appCachekey = $"AuthCodeClientId:{code}";
            //Cache authorization code
            _cachelper.StringSet<CurrentUserModel>(key, currentUserModel, TimeSpan.FromMinutes(10));
            //Which application is the cache authorization code
            _cachelper.StringSet<string>(appCachekey, appHSSetting.clientId, TimeSpan.FromMinutes(10));

            //Cache authorization code fetching token Maximum effective time
            DateTime expirTime = _cachelper.StringGet<DateTime>($"SessionExpiryKey:{sessionCode}");
            _cachelper.StringSet<DateTime>($"AuthCodeSessionTime:{code}", expirTime, expirTime - DateTime.Now);

            result.SetSuccess(code);
            return result;

        }

        /// <summary>
        /// Refresh by Token obtain Token
        /// </summary>
        /// <param name="refreshToken"></param>
        /// <param name="clientId"></param>
        /// <returns></returns>
        public string GetTokenByRefresh(string refreshToken, string clientId)
        {
            //Refresh Token Whether to cache
            CurrentUserModel currentUserModel = _cachelper.StringGet<CurrentUserModel>($"RefreshToken:{refreshToken}");
            if(currentUserModel==null)
            {
                return String.Empty;
            }
            //Refresh token Expiration time
            DateTime refreshTokenExpiry = _cachelper.StringGet<DateTime>($"RefreshTokenExpiry:{refreshToken}");
            //token The default time is 600 s
            double tokenExpiry = 600;
            //If refresh token Has expired less than 600 s Yes, token Expires on refresh token Expiration time of
            if(refreshTokenExpiry>DateTime.Now&&refreshTokenExpiry<DateTime.Now.AddSeconds(600))
            {
                tokenExpiry = (refreshTokenExpiry - DateTime.Now).TotalSeconds;
            }

                //Generate from New Token
                string token = IssueToken(currentUserModel, clientId, tokenExpiry);
                return token;

        }

        /// <summary>
        /// According to authorization code,obtain Token
        /// </summary>
        /// <param name="userInfo"></param>
        /// <param name="appHSSetting"></param>
        /// <returns></returns>
        public ResponseModel<GetTokenDTO> GetTokenWithRefresh(string authCode)
        {
            ResponseModel<GetTokenDTO> result = new ResponseModel<GetTokenDTO>();

            string key = $"AuthCode:{authCode}";
            string clientIdCachekey = $"AuthCodeClientId:{authCode}";
            string AuthCodeSessionTimeKey = $"AuthCodeSessionTime:{authCode}";

            //Obtain user information according to authorization code
            CurrentUserModel currentUserModel = _cachelper.StringGet<CurrentUserModel>(key);
            if (currentUserModel == null)
            {
                throw new Exception("code invalid");
            }
            //clean up authCode,Can only be used once
            _cachelper.DeleteKey(key);

            //Get app configuration
            string clientId = _cachelper.StringGet<string>(clientIdCachekey);
            //Refresh token Expiration time
            DateTime sessionExpiryTime = _cachelper.StringGet<DateTime>(AuthCodeSessionTimeKey);
            DateTime tokenExpiryTime = DateTime.Now.AddMinutes(10);//token Expiration time 10 minutes
             //If refresh token With expiration ratio token The default time is short, and token Set expiration time to and refresh token equally
            if (sessionExpiryTime > DateTime.Now && sessionExpiryTime < tokenExpiryTime)
            {
                tokenExpiryTime = sessionExpiryTime;
            }
            //Get access token
            string token = this.IssueToken(currentUserModel, clientId, (sessionExpiryTime - DateTime.Now).TotalSeconds);


            TimeSpan refreshTokenExpiry;
            if (sessionExpiryTime != default(DateTime))
            {
                refreshTokenExpiry = sessionExpiryTime - DateTime.Now;
            }
            else
            {
                refreshTokenExpiry = TimeSpan.FromSeconds(60 * 60 * 24);//Default 24 hours
            }
            //Get refresh token
            string refreshToken = this.IssueToken(currentUserModel, clientId, refreshTokenExpiry.TotalSeconds);
            //Cache refresh token
            _cachelper.StringSet($"RefreshToken:{refreshToken}", currentUserModel, refreshTokenExpiry);
            //Cache refresh token Expiration time
            _cachelper.StringSet($"RefreshTokenExpiry:{refreshToken}",DateTime.Now.AddSeconds(refreshTokenExpiry.TotalSeconds), refreshTokenExpiry);
            result.SetSuccess(new GetTokenDTO() { token = token, refreshToken = refreshToken, expires = 60 * 10 });
            Console.WriteLine($"client_id:{clientId}obtain token,Period of validity:{sessionExpiryTime.ToString("yyyy-MM-dd HH:mm:ss")},token:{token}");
            return result;
        }

        #region private
        /// <summary>
        /// Issued token
        /// </summary>
        /// <param name="userModel"></param>
        /// <param name="clientId"></param>
        /// <param name="second"></param>
        /// <returns></returns>
        private string IssueToken(CurrentUserModel userModel, string clientId, double second = 600)
        {
            var claims = new[]
            {
                   new Claim(ClaimTypes.Name, userModel.name),
                   new Claim("Account", userModel.account),
                   new Claim("Id", userModel.id.ToString()),
                   new Claim("Mobile", userModel.mobile),
                   new Claim(ClaimTypes.Role,userModel.role),
            };
            //var appHSSetting = getAppInfoByAppKey(clientId);
            //var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(appHSSetting.clientSecret));
            //var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
            var creds = GetCreds(clientId);
            /**
             * Claims (Payload)
                Claims This section contains some important information about the token. JWT standard specifies some fields, and some fields are excerpted below:
                iss: The issuer of the token,Issued by who
                sub: The subject of the token,token theme
                aud: Receiving object, to whom
                exp: Expiration Time.  token Expiration time, Unix timestamp format
                iat: Issued At.  token Creation time, Unix timestamp format
                jti: JWT ID. Unique identifier for the current token
                In addition to the specified fields, any other JSON compatible fields can be included.
             * */
            var token = new JwtSecurityToken(
                issuer: "SSOCenter", //Who gave it
                audience: clientId, //For whom
                claims: claims,
                expires: DateTime.Now.AddSeconds(second),//token Period of validity
                notBefore: null,//Effective immediately  DateTime.Now.AddMilliseconds(30),//30s Effective after
                signingCredentials: creds);
            string returnToken = new JwtSecurityTokenHandler().WriteToken(token);
            return returnToken;
        }

        /// <summary>
        /// according to appKey Get application information
        /// </summary>
        /// <param name="clientId"></param>
        /// <returns></returns>
        private AppHSSetting getAppInfoByAppKey(string clientId)
        {
            AppHSSetting appHSSetting = _appSettingOptions.Value.appHSSettings.Where(s => s.clientId == clientId).FirstOrDefault();
            return appHSSetting;
        }
        /// <summary>
        /// Get encryption method
        /// </summary>
        /// <returns></returns>
        protected abstract SigningCredentials GetCreds(string clientId);
        
        #endregion
    }

 

Create a new class JWTHSService to implement symmetric encryption

 /// <summary>
    /// JWT Symmetric reversible encryption
    /// </summary>
    public class JWTHSService : JWTBaseService
    {
        public JWTHSService(IOptions<AppSettingOptions> options, Cachelper cachelper):base(options,cachelper)
        {

        }
        /// <summary>
        /// Generate symmetric encryption signature certificate
        /// </summary>
        /// <param name="clientId"></param>
        /// <returns></returns>
        protected override SigningCredentials GetCreds(string clientId)
        {
           var appHSSettings=getAppInfoByAppKey(clientId);
            var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(appHSSettings.clientSecret));
            var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
            return creds;
        }
        /// <summary>
        /// according to appKey Get application information
        /// </summary>
        /// <param name="clientId"></param>
        /// <returns></returns>
        private AppHSSetting getAppInfoByAppKey(string clientId)
        {
            AppHSSetting appHSSetting = _appSettingOptions.Value.appHSSettings.Where(s => s.clientId == clientId).FirstOrDefault();
            return appHSSetting;
        }
       
    }

Create a new JWTRSService class to implement asymmetric encryption, and only one of the above symmetric encryption is required. Here we write both

 

/// <summary>
    /// JWT Asymmetric encryption
    /// </summary>
    public class JWTRSService : JWTBaseService
    {
  
        public JWTRSService(IOptions<AppSettingOptions> options, Cachelper cachelper):base(options, cachelper)
        {
 
        }
        /// <summary>
        /// Generate asymmetric encryption signature certificate
        /// </summary>
        /// <param name="clientId"></param>
        /// <returns></returns>
        protected override SigningCredentials GetCreds(string clientId)
        {
            var appRSSetting = getAppInfoByAppKey(clientId);
            var rsa = RSA.Create();
            byte[] privateKey = Convert.FromBase64String(appRSSetting.privateKey);//Only the private key is required here, no begin,No end
            rsa.ImportPkcs8PrivateKey(privateKey, out _);
            var key = new RsaSecurityKey(rsa);
            var creds = new SigningCredentials(key, SecurityAlgorithms.RsaSha256);
            return creds;
        }
        /// <summary>
        /// according to appKey Get application information
        /// </summary>
        /// <param name="clientId"></param>
        /// <returns></returns>
        private AppRSSetting getAppInfoByAppKey(string clientId)
        {
            AppRSSetting appRSSetting = _appSettingOptions.Value.appRSSettings.Where(s => s.clientId == clientId).FirstOrDefault();
            return appRSSetting;
        }

    }

When to use JWT symmetric encryption and when to use JWT asymmetric encryption?

Symmetric encryption: both parties save the same key, so the signature speed is fast. However, because the keys of both parties are the same, the security is lower than that of asymmetric encryption.

Asymmetric encryption: the authenticator saves the private key and the system saves the public key. The signing speed is slower than symmetric encryption, but the public and private keys cannot be derived from each other, so the security is high.

Therefore, symmetric encryption is used for performance-oriented and asymmetric encryption is used for security oriented. Generally, symmetric encryption is used for company systems and asymmetric encryption is used for third-party access.

web1 project:

Appsettings JSON stores web1 information

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "SSOSetting": {
    "issuer": "SSOCenter",
    "audience": "web1",
    "clientId": "web1",
    "clientSecret": "Nu4Ohg8mfpPnNxnXu53W4g0yWLqF0mX2"
  }
}

Program Add authentication code to CS file and builder Services Addauthentication(), and add app.UseAuthentication(), the complete code is as follows:

using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using RSAExtensions;
using SSO.Demo.Web1.Models;
using SSO.Demo.Web1.Utils;
using System.Security.Cryptography;
using System.Text;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddControllersWithViews();
builder.Services.AddHttpClient();
builder.Services.AddSingleton<Cachelper>();
builder.Services.Configure<AppOptions>(builder.Configuration.GetSection("AppOptions"));
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.TokenValidationParameters = new TokenValidationParameters
        {
            //Audience,Issuer,clientSecret And sso Consistency of

            //JWT There are some default attributes that can be filtered during authentication
            ValidateIssuer = true,//Verify or not Issuer
            ValidateAudience = true,//Verify or not Audience
            ValidateLifetime = true,//Whether to verify the expiration time
            ValidateIssuerSigningKey = true,//Verify or not client secret
            ValidIssuer = builder.Configuration["SSOSetting:issuer"],//
            ValidAudience = builder.Configuration["SSOSetting:audience"],//Issuer,These two items and the previous issue jwt The settings of are consistent
            IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["SSOSetting:clientSecret"]))//client secret
        };
    });

#region Asymmetric encryption-authentication 
//var rsa = RSA.Create();
//byte[] publickey = Convert.FromBase64String(AppSetting.PublicKey); //Public key, remove begin...  end ...
////rsa.ImportPkcs8PublicKey Is an extension method derived from RSAExtensions package
//rsa.ImportPkcs8PublicKey(publickey);
//var key = new RsaSecurityKey(rsa);
//var signingCredentials = new SigningCredentials(key, SecurityAlgorithms.RsaPKCS1);

//builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
//    .AddJwtBearer(options =>
//    {
//        options.TokenValidationParameters = new TokenValidationParameters
//        {
//            //Audience,Issuer,clientSecret And sso Consistency of

//            //JWT There are some default attributes that can be filtered during authentication
//            ValidateIssuer = true,//Verify or not Issuer
//            ValidateAudience = true,//Verify or not Audience
//            ValidateLifetime = true,//Whether to verify the expiration time
//            ValidateIssuerSigningKey = true,//Verify or not client secret
//            ValidIssuer = builder.Configuration["SSOSetting:issuer"],//
//            ValidAudience = builder.Configuration["SSOSetting:audience"],//Issuer,These two items and the previous issue jwt The settings of are consistent
//            IssuerSigningKey = signingCredentials.Key
//        };
//    });

#endregion



var app = builder.Build();
ServiceLocator.Instance = app.Services; //For manual acquisition DI object
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Home/Error");
    // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();
app.UseAuthentication();//Add this to UseAuthorization ago
app.UseAuthorization();


app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");

app.Run();

Then add the interface to obtain the token according to the authorization code and add the AccountController

 /// <summary>
    /// User information
    /// </summary>

    public class AccountController : Controller
    {
        private IHttpClientFactory _httpClientFactory;
        private readonly Cachelper _cachelper;
        public AccountController(IHttpClientFactory httpClientFactory, Cachelper cachelper)
        {
            _httpClientFactory = httpClientFactory;
            _cachelper = cachelper;
        }

        /// <summary>
        /// To obtain user information, the interface needs permission verification
        /// </summary>
        /// <returns></returns>
        [MyAuthorize]
        [HttpPost]
        public ResponseModel<UserDTO> GetUserInfo()
        {
            ResponseModel<UserDTO> user = new ResponseModel<UserDTO>();
            return user;
        }
        /// <summary>
        /// Login success callback
        /// </summary>
        /// <returns></returns>
        public ActionResult LoginRedirect()
        {
            return View();
        }
        //according to authCode obtain token
        [HttpPost]
        public async Task<ResponseModel<GetTokenDTO>> GetAccessCode([FromBody] GetAccessCodeRequest request)
        {
            ResponseModel<GetTokenDTO> result = new ResponseModel<GetTokenDTO>();
            //request SSO obtain token
            var client = _httpClientFactory.CreateClient();
            var param = new { authCode = request.authCode };
            string jsonData = System.Text.Json.JsonSerializer.Serialize(param);
            StringContent paramContent = new StringContent(jsonData);

            //request sso obtain token
            var response = await client.PostAsync("https://localhost:7000/SSO/GetToken", new StringContent(jsonData, Encoding.UTF8, "application/json"));
            string resultStr = await response.Content.ReadAsStringAsync();
            result = System.Text.Json.JsonSerializer.Deserialize<ResponseModel<GetTokenDTO>>(resultStr);
            if (result.code == 0) //success
            {
                //success,cache token To local session
                string token = result.data.token;
                string key = $"SessionCode:{request.sessionCode}";
                string tokenKey = $"token:{token}";
                _cachelper.StringSet<string>(key, token, TimeSpan.FromSeconds(result.data.expires));
                _cachelper.StringSet<bool>(tokenKey, true, TimeSpan.FromSeconds(result.data.expires));
                Console.WriteLine($"obtain token Success, local session code:{request.sessionCode},{Environment.NewLine}token:{token}");
            }

            return result;
        }
        /// <summary>
        /// Log out
        /// </summary>
        /// <param name="request"></param>
        /// <returns></returns>
        [HttpPost]
        public  ResponseModel LogOut([FromBody] LogOutRequest request)
        {
            string key = $"SessionCode:{request.SessionCode}";
            //Fetch based on session token
            string token = _cachelper.StringGet<string>(key);
            if (!string.IsNullOrEmpty(token))
            {
                //clean up token
                string tokenKey = $"token:{token}";
                _cachelper.DeleteKey(tokenKey);
            }
            Console.WriteLine($"session Code:{request.SessionCode}Log out");
            return new ResponseModel().SetSuccess();
        }
    }

 

In addition, the obtained token has not expired. If I log out, how can I judge that the session token is invalid?

Here, the authentication filter needs to be intercepted to judge that the token is deleted in the cache. If the authentication fails, the file MyAuthorize is added

  /// <summary>
    /// Intercept authentication filter
    /// </summary>
    public class MyAuthorize : Attribute, IAuthorizationFilter
    {
        private static Cachelper _cachelper = ServiceLocator.Instance.GetService<Cachelper>();

        public void OnAuthorization(AuthorizationFilterContext context)
        {
            string id = context.HttpContext.User.FindFirst("id")?.Value;
            if(string.IsNullOrEmpty(id))
            {
                //token Inspection failed
                context.Result = new StatusCodeResult(401); //Authentication failure returned
                return;
            }

            Console.WriteLine("I am Authorization filter");
            //Requested address
            var url = context.HttpContext.Request.Path.Value;
            //Get print header information
            var heads = context.HttpContext.Request.Headers;

            //Get token "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoi5byg5LiJIiwiQWNjb3VudCI6ImFkbWluIiwiSWQiOiIxMDEiLCJNb2JpbGUiOiIxMzgwMDEzODAwMCIsImh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vd3MvMjAwOC8wNi9pZGVudGl0eS9jbGFpbXMvcm9sZSI6IlN1cGVyQWRtaW4iLCJleHAiOjE2NTMwNjA0MDIsImlzcyI6IlNTT0NlbnRlciIsImF1ZCI6IndlYjIifQ.aAi5a0zr_nLQQaSxSBqEhHZQ6ALFD_rWn2tnLt38DeA"
            string token = heads["Authorization"];
            token = token.Replace("Bearer", "").TrimStart();//Remove "Bearer "Is the real token
            if (string.IsNullOrEmpty(token))
            {
                Console.WriteLine("Verification failed");
                return;
            }
            //redis Verify this token To determine the effectiveness of the source sso And confirm that the session has not expired
            string tokenKey = $"token:{token}";
            bool isVaid = _cachelper.StringGet<bool>(tokenKey);
            //token invalid
            if (isVaid == false)
            {
                Console.WriteLine($"token invalid,token:{token}");
                context.Result = new StatusCodeResult(401); //Authentication failure returned
            }
        }
    }

Then the controller or method to be authenticated can be automatically authenticated by adding [MyAuthorize] to its header.

web1 required login page

@{
    ViewData["Title"] = "Home Page";
}

<div class="text-center">
       <h1 class="display-4">Welcome to Web1</h1>
    <p>Learn about <a href="https://web2.com:7002">Jump to Web2</a>.</p>
        <p>Learn about <a onclick="logOut()" href="javascript:void(0);">Log out</a>.</p>
</div>
@section Scripts{
    <script src="~/js/Common.js"></script>
<script>
                    getUserInfo()
            //Get user information
            function getUserInfo(){
                //1.cookie Yes no token
                const token=getCookie('token')
                console.log('gettoken',token)
                if(!token)
                {
                    redirectLogin()
                }
                $.ajax({
          type: 'POST',
          url: '/Account/GetUserInfo',
          headers:{"Authorization":'Bearer ' + token},
          success: success,
          error:error
        });
            }
            function success(){
                console.log('success')
            }
            function error(xhr, exception){
                if(xhr.status===401) //Authentication failed
                {
                    console.log('Unauthenticated')
                    redirectLogin()
                }
            }
                      //Redirect to login
            function redirectLogin(){
                     window.location.href="https://sso.com:7000/SSO/Login?clientId=web1&redirectUrl=https://web1.com:7001/Account/LoginRedirect"
            }
            //Log out
            function logOut(){
                clearCookie("token") //clean up cookie token
                 clearCookie("refreshToken") //clean up cookie refreshToken
                  clearCookie("sessionCode")  //clean up cookie session

                  //Jump to SSO Log out
                    window.location.href="https://sso.com:7000/SSO/LogOut?clientId=web1&redirectUrl=https://web1.com:7001/Account/LoginRedirect"
               
            }

</script>
}

Go back to web1 page after sso login

@*
    For more information on enabling MVC for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860
*@
@{
    Layout = null;
}
   <script src="~/lib/jquery/dist/jquery.min.js"></script>
      <script src="~/js/Common.js"></script>
   <script>
       GetAccessToken();
       //according to code obtain token
       function GetAccessToken(){
   
          var params=GetParam()
                  //code
          var authCode=params["authCode"]
          var sessionCode=params["sessionCode"]
          console.log('authcode',authCode)
          var params={authCode,sessionCode}     
$.ajax({
  url:'/Account/GetAccessCode',
  type:"POST",
  data:JSON.stringify(params),
  contentType:"application/json; charset=utf-8",
  dataType:"json",
  success: function(data){
     console.log('token',data)
     if(data.code===0) //success
     { 
         console.log('set up cookie')
         //hold token Save to cookie,Expires on token One minute less effective time
         setCookie("token",data.data.token,data.data.expires-60,"/")
         //Refresh token,Valid for 1 day
         setCookie("refreshToken",data.data.refreshToken,24*60*60,"/")
         setCookie("SessionCode",sessionCode,24*60*60,"/")
         //Jump to home page
          window.location.href="/Home/Index"
     }
  }})

       }
           
   </script>

At this point, the core code of web1 is completed. Except for the encryption key in web1, the code of web2 is the same as that of web1. The code will not be posted. The source code is provided later.

Here, you can log in at one place and log in at all.

2. One exit, all exit

The process of quitting at one place and everywhere is like the flow chart in achieving the goal. web1 system exits and jumps to SSO. SSO sends an http request to exit other systems and jumps back to the login page.

The core problem of exiting is that SSO can only allow all systems to exit from the current browser. This is A metaphor for user A to log in to the browser of computer 1 and the browser of computer 2. Exiting from computer 1 can only log out of the browser of computer 1,

The login of computer 2 is not affected. web1 exits. The http request in SSO exits web2 without a browser request. How does web2 know to clear the token?

Here, a global session needs to be generated when SSO logs in. The SSO cookie can then generate a global code. Each system will bring a cache key as a token when logging in, so as to ensure that the local session cache key of all systems is the same,

When logging out, you only need to delete the token of the cached key.

SSO login Cshtml

@*
    For more information on enabling MVC for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860
*@
@{
}
<form id="form">
    <div>User name:<input type="text" id=userName name="userName" /></div>
    <div>Password:<input type="password" id="password" name="password" /></div>
    <div><input type="button" value="Submit" onclick="login()" /></div>
</form>

<script src="~/lib/jquery/dist/jquery.min.js"></script>
<script src="~/js/Common.js"></script>
<script>
    sessionCheck();
    //Session check
    function sessionCheck(){
          //Get parameter collection
            const urlParams=GetParam();
            const clientId=urlParams['clientId'];
            const redirectUrl=urlParams['redirectUrl']
            const sessionCode=getCookie("SessionCode")
            if(!sessionCode)
            {
                return;
            }
            //Obtain according to authorization code code
            var params={clientId,sessionCode}
            $.ajax({
            url:'/SSO/GetCodeBySessionCode',
            data:JSON.stringify(params),
            method:'post',
            dataType:'json',
            contentType:'application/json',
            success:function(data){
                if(data.code===0)
                {
                     const code=data.data
                      window.location.href=redirectUrl+'?authCode='+code+"&sessionCode="+sessionCode
                }
            }
            })
    }

        function login(){
            //Get parameter collection
            const urlParams=GetParam();

            const clientId=urlParams['clientId'];
            const redirectUrl=urlParams['redirectUrl']
                const userName=$("#userName").val()
                const password=$("#password").val()
                const params={clientId,userName,password}
            $.ajax({
                    url:'/SSO/GetCode',
                    data:JSON.stringify(params),
                    method:'post',
                    dataType:'json',
                    contentType:"application/json",
                    success:function(data){
                        //obtain code,Jump back to customer page
                        if(data.code===0)
                        {    
                        const code=data.data

                       //Store session,You'd better subtract a few minutes from the time here, or it's over there token Expired. It's just a few seconds since I signed in again
                        setCookie("SessionCode",code,24*60*60,"/")
                      window.location.href=redirectUrl+'?authCode='+code+'&sessionCode='+code
                        }
                
                    }
                })
            }
            
       
</script>

The SessionCode here is the key. As a global code, the system login will be synchronized to systems for unified logout

SSO logout Cshtml

@*
    For more information on enabling MVC for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860
*@
@{
}
<p>Logging out...</p>
<script src="~/lib/jquery/dist/jquery.min.js"></script>
<script src="~/js/Common.js?v=1"></script>
<script>
      logOut()
      function logOut()
      {
          var sessionCode=getCookie("SessionCode")
      //Clear session
        clearCookie("SessionCode")
        //Get parameter collection
              const urlParams=GetParam();
        //Jump to login
          const clientId=urlParams['clientId'];
              const redirectUrl=urlParams['redirectUrl']

              var params={sessionCode}
              //Log out
              $.ajax({
    url:'/SSO/LogOutApp',
    type:"POST",
    data:JSON.stringify(params),
    contentType:"application/json; charset=utf-8",
    dataType:"json",
    success: function(data){
       console.log('token',data)
       if(data.code===0) //success
       {
           //Jump to login page
            window.location.href='/SSO/Login'+'?clientId='+clientId+'&redirectUrl='+redirectUrl
       }
    }})


      }

</script>

Exit login interface:

     /// <summary>
        /// Log out
        /// </summary>
        /// <param name="request"></param>
        /// <returns></returns>
        [HttpPost]
        public async Task<ResponseModel> LogOutApp([FromBody] LogOutRequest request)
        {
            //Delete global session
            string sessionKey = $"SessionCode:{request.sessionCode}";
            _cachelper.DeleteKey(sessionKey);
            var client = _httpClientFactory.CreateClient();
            var param = new { sessionCode = request.sessionCode };
            string jsonData = System.Text.Json.JsonSerializer.Serialize(param);
            StringContent paramContent = new StringContent(jsonData);

            //In practice, the database or cache is used to fetch
            List<string> urls = new List<string>()
            {
                "https://localhost:7001/Account/LogOut",
                "https://localhost:7002/Account/LogOut"
            };
            //It can be asynchronous here mq Process without blocking return
            foreach (var url in urls)
            {
                //web1 Log out
                var logOutResponse = await client.PostAsync(url, new StringContent(jsonData, Encoding.UTF8, "application/json"));
                string resultStr = await logOutResponse.Content.ReadAsStringAsync();
                ResponseModel response = System.Text.Json.JsonSerializer.Deserialize<ResponseModel>(resultStr);
                if (response.code == 0) //success
                {
                    Console.WriteLine($"url:{url},session Id:{request.sessionCode},Log out successfully");
                }
                else
                {
                    Console.WriteLine($"url:{url},session Id:{request.sessionCode},Failed to log out");
                }
            };
            return new ResponseModel().SetSuccess();

        }

Exit login interface of web1 and web2

     /// <summary>
        /// Log out
        /// </summary>
        /// <param name="request"></param>
        /// <returns></returns>
        [HttpPost]
        public  ResponseModel LogOut([FromBody] LogOutRequest request)
        {
            string key = $"SessionCode:{request.SessionCode}";
            //Fetch based on session token
            string token = _cachelper.StringGet<string>(key);
            if (!string.IsNullOrEmpty(token))
            {
                //clean up token
                string tokenKey = $"token:{token}";
                _cachelper.DeleteKey(tokenKey);
            }
            Console.WriteLine($"session Code:{request.SessionCode}Log out");
            return new ResponseModel().SetSuccess();
        }

Here, one exit and all exits are completed.

3. Implementation of double token mechanism

Token and refresh_ The token generation algorithm is the same. The validity end of the knowledge token is refresh_ The valid period of the token is long.

How do you know that a refresh token is a refresh token when refreshing a token? When SSO generates a refresh token, it saves it in the cache. When refreshing a token, it is judged that there is a refresh token in the cache.

Code for generating double Tokens:

     /// <summary>
        /// According to authorization code,obtain Token
        /// </summary>
        /// <param name="userInfo"></param>
        /// <param name="appHSSetting"></param>
        /// <returns></returns>
        public ResponseModel<GetTokenDTO> GetTokenWithRefresh(string authCode)
        {
            ResponseModel<GetTokenDTO> result = new ResponseModel<GetTokenDTO>();

            string key = $"AuthCode:{authCode}";
            string clientIdCachekey = $"AuthCodeClientId:{authCode}";
            string AuthCodeSessionTimeKey = $"AuthCodeSessionTime:{authCode}";

            //Obtain user information according to authorization code
            CurrentUserModel currentUserModel = _cachelper.StringGet<CurrentUserModel>(key);
            if (currentUserModel == null)
            {
                throw new Exception("code invalid");
            }
            //clean up authCode,Can only be used once
            _cachelper.DeleteKey(key);

            //Get app configuration
            string clientId = _cachelper.StringGet<string>(clientIdCachekey);
            //Refresh token Expiration time
            DateTime sessionExpiryTime = _cachelper.StringGet<DateTime>(AuthCodeSessionTimeKey);
            DateTime tokenExpiryTime = DateTime.Now.AddMinutes(10);//token Expiration time 10 minutes
             //If refresh token With expiration ratio token The default time is short, and token Set expiration time to and refresh token equally
            if (sessionExpiryTime > DateTime.Now && sessionExpiryTime < tokenExpiryTime)
            {
                tokenExpiryTime = sessionExpiryTime;
            }
            //Get access token
            string token = this.IssueToken(currentUserModel, clientId, (sessionExpiryTime - DateTime.Now).TotalSeconds);


            TimeSpan refreshTokenExpiry;
            if (sessionExpiryTime != default(DateTime))
            {
                refreshTokenExpiry = sessionExpiryTime - DateTime.Now;
            }
            else
            {
                refreshTokenExpiry = TimeSpan.FromSeconds(60 * 60 * 24);//Default 24 hours
            }
            //Get refresh token
            string refreshToken = this.IssueToken(currentUserModel, clientId, refreshTokenExpiry.TotalSeconds);
            //Cache refresh token
            _cachelper.StringSet(refreshToken, currentUserModel, refreshTokenExpiry);
            result.SetSuccess(new GetTokenDTO() { token = token, refreshToken = refreshToken, expires = 60 * 10 });
            Console.WriteLine($"client_id:{clientId}obtain token,Period of validity:{sessionExpiryTime.ToString("yyyy-MM-dd HH:mm:ss")},token:{token}");
            return result;
        }

Get the token code according to the refresh token:

     /// <summary>
        /// Refresh by Token obtain Token
        /// </summary>
        /// <param name="refreshToken"></param>
        /// <param name="clientId"></param>
        /// <returns></returns>
        public string GetTokenByRefresh(string refreshToken, string clientId)
        {
            //Refresh Token Whether to cache
            CurrentUserModel currentUserModel = _cachelper.StringGet<CurrentUserModel>($"RefreshToken:{refreshToken}");
            if(currentUserModel==null)
            {
                return String.Empty;
            }
            //Refresh token Expiration time
            DateTime refreshTokenExpiry = _cachelper.StringGet<DateTime>($"RefreshTokenExpiry:{refreshToken}");
            //token The default time is 600 s
            double tokenExpiry = 600;
            //If refresh token Has expired less than 600 s Yes, token Expires on refresh token Expiration time of
            if(refreshTokenExpiry>DateTime.Now&&refreshTokenExpiry<DateTime.Now.AddSeconds(600))
            {
                tokenExpiry = (refreshTokenExpiry - DateTime.Now).TotalSeconds;
            }

                //Generate from New Token
                string token = IssueToken(currentUserModel, clientId, tokenExpiry);
                return token;

        }

 

4, Effect demonstration

The SSO address of the project here is: https://localhost:7000 , web1 address is: https://localhost:7001 , the web2 address is: https://localhost:7002

Modify the hosts file so that cookie s cannot be shared under different domain names.

win10 path: C:\Windows\System32\drivers\etc\hosts added at the end

127.0.0.1 sso.com
127.0.0.1 web1.com
127.0.0.1 web2.com

In this way, a new address, SSO address, is obtained: https://sso.com:7000 , web1 address is: https://web1.com , the web2 address is: https://web2.com

 

 

1. Here at the beginning, visit https://web2.com Not logged in jump to https://sso.com .

2. Then visit https://web1.com I didn't log in, and I jumped to https://sso.com , proving that web1 and web2 are not logged in.

3. After logging in to the sso, jump back to web1, and then click https://web2.com Connection to jump to https://web2.com , automatically logged in.

4. Then log out in web1, refresh the page in web2, and log out.

Take another look at the records of SSO log printing under these operations.

Come here Net 6. SSO based on JWT+OAuth2.0 is completed.

 

Finally, the source code is attached: https://github.com/weixiaolong325/SSO.Demo.SSO

Tags: .NET

Posted by hiroshi_satori on Tue, 31 May 2022 06:15:29 +0530