目錄 "identityserver4源碼解析_1_項目結構" "identityserver4源碼解析_2_元數據介面" "identityserver4源碼解析_3_認證介面" "identityserver4源碼解析_4_令牌發放介面" "identityserver4源碼解析_5_查詢用戶信 ...
目錄
- IdentityServer4源碼解析_1_項目結構
- IdentityServer4源碼解析_2_元數據介面
- IdentityServer4源碼解析_3_認證介面
- IdentityServer4源碼解析_4_令牌發放介面
- IdentityServer4源碼解析_5_查詢用戶信息介面
- [IdentityServer4源碼解析_6_結束會話介面]
- [IdentityServer4源碼解析_7_查詢令牌信息介面]
- [IdentityServer4源碼解析_8_撤銷令牌介面]
協議
Token介面
oidc服務需要提供token介面,提供AccessToken,IdToken,以及RefreshToken(可選)。在授權碼模式下,token介面必須使用https。
請求
必須使用POST方法,使用x-www-form-urlencoded序列化參數,clientId:clientSecret使用Basic加密放在Authorization頭中
POST /token HTTP/1.1
Host: server.example.com
Content-Type: application/x-www-form-urlencoded
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
grant_type=authorization_code&code=SplxlOBeZQQYbYS6WxSbIA
&redirect_uri=https%3A%2F%2Fclient.example.org%2Fcb
請求校驗
認證服務必須校驗下列內容:
- 驗證client是否頒發了秘鑰
- 驗證為該客戶端頒發了授權碼
- 驗證授權碼有效性
- 如果可能的話,驗證授權碼是否被使用過
- 驗證redirect_uri 與發起認證請求時的值一致
成功響應
在收到token請求,並校驗通過之後,認證服務返回成功報文,報文包含了身份令牌和通行令牌。數據格式使用application/json。token_type必須返回Bearer,其他類型token不在本協議範圍內。在OAuth2.0響應報文基礎上,oidc增加了id_tken。所有token包含了token或者其他敏感信息的響應報文,必須包含以下響應頭。
Cache-Control no-store
Pragma no-cache
失敗響應
如果認證失敗返回application/json格式錯誤消息,狀態碼400
HTTP/1.1 400 Bad Request
Content-Type: application/json
Cache-Control: no-store
Pragma: no-cache
{
"error": "invalid_request"
}
id token校驗
客戶端必須校驗返回的id token, 校驗條件如下。對照這些條件,就可以更懂Microsoft.Authentication.OpenIdConnect裡面的代碼了,要做的事情很多。
- 如果id token被加密,使用客戶端註冊時候約定的秘鑰和演算法解密。如果約定了加密方式,id token未被加密,客戶端應該拒絕。
- 簽發方標識必須與iss聲明一致
- 客戶端必須校驗aud聲明包含了它的客戶端id,如果id token未返回正確的audience或者反悔了不被新人的audience,應該拒絕
- 如果id token包含多個audience,需要校驗是否有azp聲明。azp即Authorized party,標識被授權的client。
- 如果包含azp聲明,客戶端需要校驗其值是否為自己的客戶端id
- 如果id token由token介面直接頒發給客戶端(授權碼模式就是如此),客戶端必鬚根據alg參數值的演算法驗證簽名。客戶端必須使用簽發方提供的秘鑰。
- alg值預設為RS256,客戶端可以在註冊的時候使用id_token_signed_response_alg參數指定配置。
- 如果jwt的alg頭使用了基於mac地址的加密演算法,如HS256, HS384,HS512,aud聲明中的位元組會用作驗簽。(意思是會把mac地址相關信息寫在aud聲明上?)
- The current time MUST be before the time represented by the exp Claim.
當前時間必須早於exp(token過期時間)。 - iat(簽發時間)可以用於拒絕過早、或者過於頻繁簽發的token,可以用於預防重放攻擊。可接受時間範圍由客戶端自行決定。
- 如果認證請求包含了nonce參數,客戶端必須交驗認證響應中返回的nonce值是否一致。防止重放攻擊。
- 如果客戶端請求了acr聲明(Authentication Context Class Reference,認證會話上下文,用於表示當前認證會話),必須交驗acr值是否合法。
- 如果客戶端請求了auth_time聲明,客戶端應該校驗認證時間是否已經超出,是否需要重新認證。
access token校驗
如果id_token中包含了at_hash聲明,需要做下麵的校驗。at_hash標明瞭access_token和id_token之間的會話關聯關係,做這個校驗可以防跨站偽造。
- 用idtoken的alg頭標明的演算法加密access_token,比如alg位RS256,則是用HSA-256演算法加密。
- 取hash值左邊一般使用base64url加密
- id token中的at_hash值必須跟上個步驟得到的值一致
校驗規則很多,瞭解一下即可,絕大部分屬於客戶端需要做的部分,絕大部分跟安全有關。這一塊的實現可以參考Microsoft.Authentication.OpenIdConnect,這是客戶端的實現。我們現在看的IdentityServer是認證服務端的實現。
源碼
五種授權模式
有下麵幾種授權模式可以請求token介面
- 授權碼模式:最常用的code換token
- 混合模式:混合模式是授權碼模式+簡化模式混合使用的方式,在用授權碼code找token介面換通行/身份令牌的邏輯與授權碼模式的邏輯是一樣的。idsv4中,混合模式沒有自己的單獨實現,只是把授權碼+簡化模式的代碼同時調用。
- 客戶端密鑰模式:一般用於完全信任的內部系統,密鑰換取access_token,由於沒有用戶參與,scope包含open_id是非法的
- 用戶名密碼模式:一般用於第三方對接、無界面交互場景。即username+password換token/id_token,password不一定是密碼,也可以是驗證碼或其他的什麼東西,這個完全取決於開發自己的實現
- 設備流模式(略)
註意:簡化模式所有的token都是由認證介面(authorize)一次性返回的,不能使用token介面。
校驗請求方法
token介面僅允許POST方法,Content-Type必須為application/x-www-form-urlencoded,否則拋出InvalidRequest錯誤。
public async Task<IEndpointResult> ProcessAsync(HttpContext context)
{
_logger.LogTrace("Processing token request.");
// validate HTTP
if (!HttpMethods.IsPost(context.Request.Method) || !context.Request.HasFormContentType)
{
_logger.LogWarning("Invalid HTTP request for token endpoint");
return Error(OidcConstants.TokenErrors.InvalidRequest);
}
return await ProcessTokenRequestAsync(context);
}
處理流程
- 校驗客戶端
- 校驗請求參數
- 創建返回值
- 返回結果
private async Task<IEndpointResult> ProcessTokenRequestAsync(HttpContext context)
{
_logger.LogDebug("Start token request.");
// validate client
var clientResult = await _clientValidator.ValidateAsync(context);
if (clientResult.Client == null)
{
return Error(OidcConstants.TokenErrors.InvalidClient);
}
// validate request
var form = (await context.Request.ReadFormAsync()).AsNameValueCollection();
_logger.LogTrace("Calling into token request validator: {type}", _requestValidator.GetType().FullName);
var requestResult = await _requestValidator.ValidateRequestAsync(form, clientResult);
if (requestResult.IsError)
{
await _events.RaiseAsync(new TokenIssuedFailureEvent(requestResult));
return Error(requestResult.Error, requestResult.ErrorDescription, requestResult.CustomResponse);
}
// create response
_logger.LogTrace("Calling into token request response generator: {type}", _responseGenerator.GetType().FullName);
var response = await _responseGenerator.ProcessAsync(requestResult);
await _events.RaiseAsync(new TokenIssuedSuccessEvent(response, requestResult));
LogTokens(response, requestResult);
// return result
_logger.LogDebug("Token request success.");
return new TokenResult(response);
}
校驗客戶端
- 解碼客戶端秘鑰,對應的處理類是BasicAuthenticationSecretParser,客戶端id和秘鑰用base64url加密方法放在Authorzaition頭上。base64url基本是明文的,因為授權碼換token是後端進行的,所以安全性沒有問題
- 解碼得到客戶端id和秘鑰之後,跟store對比校驗客戶端是否存在,秘鑰是否一致。
public async Task<ClientSecretValidationResult> ValidateAsync(HttpContext context)
{
_logger.LogDebug("Start client validation");
var fail = new ClientSecretValidationResult
{
IsError = true
};
var parsedSecret = await _parser.ParseAsync(context);
if (parsedSecret == null)
{
await RaiseFailureEventAsync("unknown", "No client id found");
_logger.LogError("No client identifier found");
return fail;
}
// load client
var client = await _clients.FindEnabledClientByIdAsync(parsedSecret.Id);
if (client == null)
{
await RaiseFailureEventAsync(parsedSecret.Id, "Unknown client");
_logger.LogError("No client with id '{clientId}' found. aborting", parsedSecret.Id);
return fail;
}
SecretValidationResult secretValidationResult = null;
if (!client.RequireClientSecret || client.IsImplicitOnly())
{
_logger.LogDebug("Public Client - skipping secret validation success");
}
else
{
secretValidationResult = await _validator.ValidateAsync(parsedSecret, client.ClientSecrets);
if (secretValidationResult.Success == false)
{
await RaiseFailureEventAsync(client.ClientId, "Invalid client secret");
_logger.LogError("Client secret validation failed for client: {clientId}.", client.ClientId);
return fail;
}
}
_logger.LogDebug("Client validation success");
var success = new ClientSecretValidationResult
{
IsError = false,
Client = client,
Secret = parsedSecret,
Confirmation = secretValidationResult?.Confirmation
};
await RaiseSuccessEventAsync(client.ClientId, parsedSecret.Type);
return success;
}
校驗請求參數
- 客戶端的PortocalType必須位oidc,否則報錯InvalidClient
- 校驗GrantType,必填,長度不能超過100。
- GrantType預設支持以下幾種類型,還可以自定義GrantType
- authorization_code:授權碼換token
- client_credentials:客戶端秘鑰換token
- password:用戶名密碼換token
- refresn_token:刷新令牌換token
- urn:ietf:params:oauth:grant-type:device_code:deviceflow,略
public async Task<TokenRequestValidationResult> ValidateRequestAsync(NameValueCollection parameters, ClientSecretValidationResult clientValidationResult)
{
_logger.LogDebug("Start token request validation");
_validatedRequest = new ValidatedTokenRequest
{
Raw = parameters ?? throw new ArgumentNullException(nameof(parameters)),
Options = _options
};
if (clientValidationResult == null) throw new ArgumentNullException(nameof(clientValidationResult));
_validatedRequest.SetClient(clientValidationResult.Client, clientValidationResult.Secret, clientValidationResult.Confirmation);
/////////////////////////////////////////////
// check client protocol type
/////////////////////////////////////////////
if (_validatedRequest.Client.ProtocolType != IdentityServerConstants.ProtocolTypes.OpenIdConnect)
{
LogError("Invalid protocol type for client",
new
{
clientId = _validatedRequest.Client.ClientId,
expectedProtocolType = IdentityServerConstants.ProtocolTypes.OpenIdConnect,
actualProtocolType = _validatedRequest.Client.ProtocolType
});
return Invalid(OidcConstants.TokenErrors.InvalidClient);
}
/////////////////////////////////////////////
// check grant type
/////////////////////////////////////////////
var grantType = parameters.Get(OidcConstants.TokenRequest.GrantType);
if (grantType.IsMissing())
{
LogError("Grant type is missing");
return Invalid(OidcConstants.TokenErrors.UnsupportedGrantType);
}
if (grantType.Length > _options.InputLengthRestrictions.GrantType)
{
LogError("Grant type is too long");
return Invalid(OidcConstants.TokenErrors.UnsupportedGrantType);
}
_validatedRequest.GrantType = grantType;
switch (grantType)
{
case OidcConstants.GrantTypes.AuthorizationCode:
return await RunValidationAsync(ValidateAuthorizationCodeRequestAsync, parameters);
case OidcConstants.GrantTypes.ClientCredentials:
return await RunValidationAsync(ValidateClientCredentialsRequestAsync, parameters);
case OidcConstants.GrantTypes.Password:
return await RunValidationAsync(ValidateResourceOwnerCredentialRequestAsync, parameters);
case OidcConstants.GrantTypes.RefreshToken:
return await RunValidationAsync(ValidateRefreshTokenRequestAsync, parameters);
case OidcConstants.GrantTypes.DeviceCode:
return await RunValidationAsync(ValidateDeviceCodeRequestAsync, parameters);
default:
return await RunValidationAsync(ValidateExtensionGrantRequestAsync, parameters);
}
}
參數校驗 - 授權碼模式
- 客戶端AllowedGrantTypes必須包含authorization_code或者hybrid,否則報錯UnauthorizedClient。
- code必填,code長度不能超過100
- 客戶端傳過來的code只是授權碼的id,從store中取出來授權碼對象,如果不存在返回錯誤InvalidGrant
- 從store中移除授權碼,此處實現了code只是用一次
- 如果授權碼超出有效時長,返回錯誤invalidGrant
- 校驗授權碼對象的客戶端id與當前客戶端是否一致
- redirect_uri必填,且必須與授權碼對象保存的redirect_uri一致,否則返回錯誤UnauthorizedClient
- 如果請求中沒有任何scope,返回錯誤invalidRequest
- 判斷用戶是否啟用,這個判斷是由可插拔服務IProfileService的IsActive方法來實現的,開發可以註入自己的實現。如果用戶禁用將返回錯誤InvalidGrant。
private async Task<TokenRequestValidationResult> ValidateAuthorizationCodeRequestAsync(NameValueCollection parameters)
{
_logger.LogDebug("Start validation of authorization code token request");
/////////////////////////////////////////////
// check if client is authorized for grant type
/////////////////////////////////////////////
if (!_validatedRequest.Client.AllowedGrantTypes.ToList().Contains(GrantType.AuthorizationCode) &&
!_validatedRequest.Client.AllowedGrantTypes.ToList().Contains(GrantType.Hybrid))
{
LogError("Client not authorized for code flow");
return Invalid(OidcConstants.TokenErrors.UnauthorizedClient);
}
/////////////////////////////////////////////
// validate authorization code
/////////////////////////////////////////////
var code = parameters.Get(OidcConstants.TokenRequest.Code);
if (code.IsMissing())
{
LogError("Authorization code is missing");
return Invalid(OidcConstants.TokenErrors.InvalidGrant);
}
if (code.Length > _options.InputLengthRestrictions.AuthorizationCode)
{
LogError("Authorization code is too long");
return Invalid(OidcConstants.TokenErrors.InvalidGrant);
}
_validatedRequest.AuthorizationCodeHandle = code;
var authZcode = await _authorizationCodeStore.GetAuthorizationCodeAsync(code);
if (authZcode == null)
{
LogError("Invalid authorization code", new { code });
return Invalid(OidcConstants.TokenErrors.InvalidGrant);
}
await _authorizationCodeStore.RemoveAuthorizationCodeAsync(code);
if (authZcode.CreationTime.HasExceeded(authZcode.Lifetime, _clock.UtcNow.UtcDateTime))
{
LogError("Authorization code expired", new { code });
return Invalid(OidcConstants.TokenErrors.InvalidGrant);
}
/////////////////////////////////////////////
// populate session id
/////////////////////////////////////////////
if (authZcode.SessionId.IsPresent())
{
_validatedRequest.SessionId = authZcode.SessionId;
}
/////////////////////////////////////////////
// validate client binding
/////////////////////////////////////////////
if (authZcode.ClientId != _validatedRequest.Client.ClientId)
{
LogError("Client is trying to use a code from a different client", new { clientId = _validatedRequest.Client.ClientId, codeClient = authZcode.ClientId });
return Invalid(OidcConstants.TokenErrors.InvalidGrant);
}
/////////////////////////////////////////////
// validate code expiration
/////////////////////////////////////////////
if (authZcode.CreationTime.HasExceeded(_validatedRequest.Client.AuthorizationCodeLifetime, _clock.UtcNow.UtcDateTime))
{
LogError("Authorization code is expired");
return Invalid(OidcConstants.TokenErrors.InvalidGrant);
}
_validatedRequest.AuthorizationCode = authZcode;
_validatedRequest.Subject = authZcode.Subject;
/////////////////////////////////////////////
// validate redirect_uri
/////////////////////////////////////////////
var redirectUri = parameters.Get(OidcConstants.TokenRequest.RedirectUri);
if (redirectUri.IsMissing())
{
LogError("Redirect URI is missing");
return Invalid(OidcConstants.TokenErrors.UnauthorizedClient);
}
if (redirectUri.Equals(_validatedRequest.AuthorizationCode.RedirectUri, StringComparison.Ordinal) == false)
{
LogError("Invalid redirect_uri", new { redirectUri, expectedRedirectUri = _validatedRequest.AuthorizationCode.RedirectUri });
return Invalid(OidcConstants.TokenErrors.UnauthorizedClient);
}
/////////////////////////////////////////////
// validate scopes are present
/////////////////////////////////////////////
if (_validatedRequest.AuthorizationCode.RequestedScopes == null ||
!_validatedRequest.AuthorizationCode.RequestedScopes.Any())
{
LogError("Authorization code has no associated scopes");
return Invalid(OidcConstants.TokenErrors.InvalidRequest);
}
/////////////////////////////////////////////
// validate PKCE parameters
/////////////////////////////////////////////
var codeVerifier = parameters.Get(OidcConstants.TokenRequest.CodeVerifier);
if (_validatedRequest.Client.RequirePkce || _validatedRequest.AuthorizationCode.CodeChallenge.IsPresent())
{
_logger.LogDebug("Client required a proof key for code exchange. Starting PKCE validation");
var proofKeyResult = ValidateAuthorizationCodeWithProofKeyParameters(codeVerifier, _validatedRequest.AuthorizationCode);
if (proofKeyResult.IsError)
{
return proofKeyResult;
}
_validatedRequest.CodeVerifier = codeVerifier;
}
else
{
if (codeVerifier.IsPresent())
{
LogError("Unexpected code_verifier: {codeVerifier}. This happens when the client is trying to use PKCE, but it is not enabled. Set RequirePkce to true.", codeVerifier);
return Invalid(OidcConstants.TokenErrors.InvalidGrant);
}
}
/////////////////////////////////////////////
// make sure user is enabled
/////////////////////////////////////////////
var isActiveCtx = new IsActiveContext(_validatedRequest.AuthorizationCode.Subject, _validatedRequest.Client, IdentityServerConstants.ProfileIsActiveCallers.AuthorizationCodeValidation);
await _profile.IsActiveAsync(isActiveCtx);
if (isActiveCtx.IsActive == false)
{
LogError("User has been disabled", new { subjectId = _validatedRequest.AuthorizationCode.Subject.GetSubjectId() });
return Invalid(OidcConstants.TokenErrors.InvalidGrant);
}
_logger.LogDebug("Validation of authorization code token request success");
return Valid();
}
參數校驗 - 客戶端秘鑰模式
- 校驗Client是否允許使用客戶端秘鑰認證模式
- 校驗客戶端是否允許訪問請求的授權範圍scope
- scope中包含openid的話返回錯誤invlidScope,因為本模式沒有涉及用戶信息
- cope中包含offline_access則返回錯誤InvalidScope,本模式不允許使用refresh_token
private async Task<TokenRequestValidationResult> ValidateClientCredentialsRequestAsync(NameValueCollection parameters)
{
_logger.LogDebug("Start client credentials token request validation");
/////////////////////////////////////////////
// check if client is authorized for grant type
/////////////////////////////////////////////
if (!_validatedRequest.Client.AllowedGrantTypes.ToList().Contains(GrantType.ClientCredentials))
{
LogError("Client not authorized for client credentials flow, check the AllowedGrantTypes setting", new { clientId = _validatedRequest.Client.ClientId });
return Invalid(OidcConstants.TokenErrors.UnauthorizedClient);
}
/////////////////////////////////////////////
// check if client is allowed to request scopes
/////////////////////////////////////////////
if (!await ValidateRequestedScopesAsync(parameters, ignoreImplicitIdentityScopes: true, ignoreImplicitOfflineAccess: true))
{
return Invalid(OidcConstants.TokenErrors.InvalidScope);
}
if (_validatedRequest.ValidatedScopes.ContainsOpenIdScopes)
{
LogError("Client cannot request OpenID scopes in client credentials flow", new { clientId = _validatedRequest.Client.ClientId });
return Invalid(OidcConstants.TokenErrors.InvalidScope);
}
if (_validatedRequest.ValidatedScopes.ContainsOfflineAccessScope)
{
LogError("Client cannot request a refresh token in client credentials flow", new { clientId = _validatedRequest.Client.ClientId });
return Invalid(OidcConstants.TokenErrors.InvalidScope);
}
_logger.LogDebug("{clientId} credentials token request validation success", _validatedRequest.Client.ClientId);
return Valid();
}
參數校驗 - 用戶名密碼模式
- 校驗客戶端是否允許使用用戶名密碼模式,校驗失敗返回UnauthoriedClient錯誤
- 校驗客戶端是否有權訪問所請求的所有scope,校驗失敗返回InvalidScope錯誤
- 從請求中獲取username和password參數,未提供username返回InvalidGrant錯誤,未提供password則設置password位空值
- username和password長度不能超過100,否則返回InvalidGrant錯誤
- 使用IResourceOwnerPasswordValidator校驗username和password,此介面需要開發實現後註入,否則會拋出異常
- GrantValidationResult的Subject不能為null,因此開發的IResourceOwnerPasswordValidator實現,校驗成功後必須給GrantValidationResult賦值
- 校驗用戶是否禁用,根據可插拔服務IProfileService的IsAcrtive方法判斷,如果禁用返回IvalidGrant錯誤。
private async Task<TokenRequestValidationResult> ValidateResourceOwnerCredentialRequestAsync(NameValueCollection parameters)
{
_logger.LogDebug("Start resource owner password token request validation");
/////////////////////////////////////////////
// check if client is authorized for grant type
/////////////////////////////////////////////
if (!_validatedRequest.Client.AllowedGrantTypes.Contains(GrantType.ResourceOwnerPassword))
{
LogError("Client not authorized for resource owner flow, check the AllowedGrantTypes setting", new { client_id = _validatedRequest.Client.ClientId });
return Invalid(OidcConstants.TokenErrors.UnauthorizedClient);
}
/////////////////////////////////////////////
// check if client is allowed to request scopes
/////////////////////////////////////////////
if (!(await ValidateRequestedScopesAsync(parameters)))
{
return Invalid(OidcConstants.TokenErrors.InvalidScope);
}
/////////////////////////////////////////////
// check resource owner credentials
/////////////////////////////////////////////
var userName = parameters.Get(OidcConstants.TokenRequest.UserName);
var password = parameters.Get(OidcConstants.TokenRequest.Password);
if (userName.IsMissing())
{
LogError("Username is missing");
return Invalid(OidcConstants.TokenErrors.InvalidGrant);
}
if (password.IsMissing())
{
password = "";
}
if (userName.Length > _options.InputLengthRestrictions.UserName ||
password.Length > _options.InputLengthRestrictions.Password)
{
LogError("Username or password too long");
return Invalid(OidcConstants.TokenErrors.InvalidGrant);
}
_validatedRequest.UserName = userName;
/////////////////////////////////////////////
// authenticate user
/////////////////////////////////////////////
var resourceOwnerContext = new ResourceOwnerPasswordValidationContext
{
UserName = userName,
Password = password,
Request = _validatedRequest
};
await _resourceOwnerValidator.ValidateAsync(resourceOwnerContext);
if (resourceOwnerContext.Result.IsError)
{
// protect against bad validator implementations
resourceOwnerContext.Result.Error = resourceOwnerContext.Result.Error ?? OidcConstants.TokenErrors.InvalidGrant;
if (resourceOwnerContext.Result.Error == OidcConstants.TokenErrors.UnsupportedGrantType)
{
LogError("Resource owner password credential grant type not supported");
await RaiseFailedResourceOwnerAuthenticationEventAsync(userName, "password grant type not supported", resourceOwnerContext.Request.Client.ClientId);
return Invalid(OidcConstants.TokenErrors.UnsupportedGrantType, customResponse: resourceOwnerContext.Result.CustomResponse);
}
var errorDescription = "invalid_username_or_password";
if (resourceOwnerContext.Result.ErrorDescription.IsPresent())
{
errorDescription = resourceOwnerContext.Result.ErrorDescription;
}
LogInformation("User authentication failed: ", errorDescription ?? resourceOwnerContext.Result.Error);
await RaiseFailedResourceOwnerAuthenticationEventAsync(userName, errorDescription, resourceOwnerContext.Request.Client.ClientId);
return Invalid(resourceOwnerContext.Result.Error, errorDescription, resourceOwnerContext.Result.CustomResponse);
}
if (resourceOwnerContext.Result.Subject == null)
{
var error = "User authentication failed: no principal returned";
LogError(error);
await RaiseFailedResourceOwnerAuthenticationEventAsync(userName, error, resourceOwnerContext.Request.Client.ClientId);
return Invalid(OidcConstants.TokenErrors.InvalidGrant);
}
/////////////////////////////////////////////
// make sure user is enabled
/////////////////////////////////////////////
var isActiveCtx = new IsActiveContext(resourceOwnerContext.Result.Subject, _validatedRequest.Client, IdentityServerConstants.ProfileIsActiveCallers.ResourceOwnerValidation);
await _profile.IsActiveAsync(isActiveCtx);
if (isActiveCtx.IsActive == false)
{
LogError("User has been disabled", new { subjectId = resourceOwnerContext.Result.Subject.GetSubjectId() });
await RaiseFailedResourceOwnerAuthenticationEventAsync(userName, "user is inactive", resourceOwnerContext.Request.Client.ClientId);
return Invalid(OidcConstants.TokenErrors.InvalidGrant);
}
_validatedRequest.UserName = userName;
_validatedRequest.Subject = resourceOwnerContext.Result.Subject;
await RaiseSuccessfulResourceOwnerAuthenticationEventAsync(userName, resourceOwnerContext.Result.Subject.GetSubjectId(), resourceOwnerContext.Request.Client.ClientId);
_logger.LogDebug("Resource owner password token request validation success.");
return Valid(resourceOwnerContext.Result.CustomResponse);
}
參數校驗 - RefreshToken
RefreshToken-刷新令牌。顧名思義,用於刷新通行令牌的憑證。擁有offline_access許可權的客戶端可以使用刷新令牌。只有授權碼、混合流程等由後端參與的授權模式才允許使用刷新令牌。
- 從請求中獲取refresh_token參數值,如果為空則返回InvalidRequest錯誤
- 如果刷新令牌長度超過100,返回InvalidGrant錯誤
- 判斷刷新令牌是否存在且有效
- 是否能從store中查詢到刷新令牌對象
- 校驗刷新令牌是否過期
- 校驗刷新令牌是否屬於當前客戶端
- 校驗客戶端是否仍然有offline_access許可權
- 校驗用戶是否被禁用
private async Task<TokenRequestValidationResult> ValidateRefreshTokenRequestAsync(NameValueCollection parameters)
{
_logger.LogDebug("Start validation of refresh token request");
var refreshTokenHandle = parameters.Get(OidcConstants.TokenRequest.RefreshToken);
if (refreshTokenHandle.IsMissing())
{
LogError("Refresh token is missing");
return Invalid(OidcConstants.TokenErrors.InvalidRequest);
}
if (refreshTokenHandle.Length > _options.InputLengthRestrictions.RefreshToken)
{
LogError("Refresh token too long");
return Invalid(OidcConstants.TokenErrors.InvalidGrant);
}
var result = await _tokenValidator.ValidateRefreshTokenAsync(refreshTokenHandle, _validatedRequest.Client);
if (result.IsError)
{
LogWarning("Refresh token validation failed. aborting");
return Invalid(OidcConstants.TokenErrors.InvalidGrant);
}
_validatedRequest.RefreshToken = result.RefreshToken;
_validatedRequest.RefreshTokenHandle = refreshTokenHandle;
_validatedRequest.Subject = result.RefreshToken.Subject;
_logger.LogDebug("Validation of refresh token request success");
return Valid();
}
生成響應報文 - 授權碼模式
- 生成accessToken和refreshToken
- 通行令牌由ITokenService介面,預設實現DefaultTokenService的CreateAccessTokenAsync方法生成
- 如果請求了offline_access才生成refresh_token
- 如果code是oidc授權碼,生成id_token。DefaultTokenService的CreateIdentityTokenAsync方法生成Token對象,CreateSecurityTokenAsync方法將Token對象加密為jwt。
protected virtual async Task<TokenResponse> ProcessAuthorizationCodeRequestAsync(TokenRequestValidationResult request)
{
Logger.LogTrace("Creating response for authorization code request");
//////////////////////////
// access token
/////////////////////////
(var accessToken, var refreshToken) = await CreateAccessTokenAsync(request.ValidatedRequest);
var response = new TokenResponse
{
AccessToken = accessToken,
AccessTokenLifetime = request.ValidatedRequest.AccessTokenLifetime,
Custom = request.CustomResponse,
Scope = request.ValidatedRequest.AuthorizationCode.RequestedScopes.ToSpaceSeparatedString(),
};
//////////////////////////
// refresh token
/////////////////////////
if (refreshToken.IsPresent())
{
response.RefreshToken = refreshToken;
}
//////////////////////////
// id token
/////////////////////////
if (request.ValidatedRequest.AuthorizationCode.IsOpenId)
{
// load the client that belongs to the authorization code
Client client = null;
if (request.ValidatedRequest.AuthorizationCode.ClientId != null)
{
client = await Clients.FindEnabledClientByIdAsync(request.ValidatedRequest.AuthorizationCode.ClientId);
}
if (client == null)
{
throw new InvalidOperationException("Client does not exist anymore.");
}
var resources = await Resources.FindEnabledResourcesByScopeAsync(request.ValidatedRequest.AuthorizationCode.RequestedScopes);
var tokenRequest = new TokenCreationRequest
{
Subject = request.ValidatedRequest.AuthorizationCode.Subject,
Resources = resources,
Nonce = request.ValidatedRequest.AuthorizationCode.Nonce,
AccessTokenToHash = response.AccessToken,
StateHash = request.ValidatedRequest.AuthorizationCode.StateHash,
ValidatedRequest = request.ValidatedRequest
};
var idToken = await TokenService.CreateIdentityTokenAsync(tokenRequest);
var jwt = await TokenService.CreateSecurityTokenAsync(idToken);
response.IdentityToken = jwt;
}
return response;
}
生成響應報文 - 客戶端密鑰模式
- 僅生成accessToken
protected virtual Task<TokenResponse> ProcessClientCredentialsRequestAsync(TokenRequestValidationResult request)
{
Logger.LogTrace("Creating response for client credentials request");
return ProcessTokenRequestAsync(request);
}
protected virtual async Task<TokenResponse> ProcessTokenRequestAsync(TokenRequestValidationResult validationResult)
{
(var accessToken, var refreshToken) = await CreateAccessTokenAsync(validationResult.ValidatedRequest);
var response = new TokenResponse
{
AccessToken = accessToken,
AccessTokenLifetime = validationResult.ValidatedRequest.AccessTokenLifetime,
Custom = validationResult.CustomResponse,
Scope = validationResult.ValidatedRequest.Scopes.ToSpaceSeparatedString()
};
if (refreshToken.IsPresent())
{
response.RefreshToken = refreshToken;
}
return response;
}
生成響應報文 - 用戶名密碼模式
- 生成accessToken
- 如果申請了offline_access且有許可權,同時返回refresh_token
- 不會返回id_token,我理解的是授權碼等模式是有限授權,需要code換id_token,才能拿到用戶id以及其他的基本信息。而密碼模式是完全信任授權,賬號密碼都給你了,還整id_token幹嘛,你要啥信息,自己實現IResourceOwnerPasswordValidator,自己去庫里取就完事了,還要啥自行車。
protected virtual Task<TokenResponse> ProcessPasswordRequestAsync(TokenRequestValidationResult request)
{
Logger.LogTrace("Creating response for password request");
return ProcessTokenRequestAsync(request);
}
生成響應報文 - 刷新令牌
- 從請求中取出舊通行令牌
- 判斷客戶端配置UpdateAccessTokenClaimsOnRefresh-是否在刷新令牌的時候更新通行令牌的claims,預設false。如果為true,則創建新的token對象,否則使用舊的token,只是刷新token的創建時間和有效時間。
- 判斷客戶端配置RefreshTokenUsage - 刷新令牌用法,0:ReUse可重覆使用 1:OnTimeOnly一次性,預設1。如果是一次性的話,從store中刪除舊的刷新令牌,創建新的刷新令牌。
- 判斷客戶端配置RefreshTokenExpiration - 刷新令牌過期類型,0:Sliding,1:Absolute,預設1。如果是0,需要重新計算相對時間。
- 如果刷新令牌請求包含了任意身份資源,創建新的身份令牌。
protected virtual async Task<TokenResponse> ProcessRefreshTokenRequestAsync(TokenRequestValidationResult request)
{
Logger.LogTrace("Creating response for refresh token request");
var oldAccessToken = request.ValidatedRequest.RefreshToken.AccessToken;
string accessTokenString;
if (request.ValidatedRequest.Client.UpdateAccessTokenClaimsOnRefresh)
{
var subject = request.ValidatedRequest.RefreshToken.Subject;
var creationRequest = new TokenCreationRequest
{
Subject = subject,
ValidatedRequest = request.ValidatedRequest,
Resources = await Resources.FindEnabledResourcesByScopeAsync(oldAccessToken.Scopes)
};
var newAccessToken = await TokenService.CreateAccessTokenAsync(creationRequest);
accessTokenString = await TokenService.CreateSecurityTokenAsync(newAccessToken);
}
else
{
oldAccessToken.CreationTime = Clock.UtcNow.UtcDateTime;
oldAccessToken.Lifetime = request.ValidatedRequest.AccessTokenLifetime;
accessTokenString = await TokenService.CreateSecurityTokenAsync(oldAccessToken);
}
var handle = await RefreshTokenService.UpdateRefreshTokenAsync(request.ValidatedRequest.RefreshTokenHandle, request.ValidatedRequest.RefreshToken, request.ValidatedRequest.Client);
return new TokenResponse
{
IdentityToken = await CreateIdTokenFromRefreshTokenRequestAsync(request.ValidatedRequest, accessTokenString),
AccessToken = accessTokenString,
AccessTokenLifetime = request.ValidatedRequest.AccessTokenLifetime,
RefreshToken = handle,
Custom = request.CustomResponse,
Scope = request.ValidatedRequest.RefreshToken.Scopes.ToSpaceSeparatedString()
};
}