IdentityServer4源码解析_3_认证接口

目录

协议

五种认证方式

认证方式特点对比

特点 授权码模式 简化模式 混合模式
所有token从Authorization接口返回 No Yes Yes
所有token从Token接口返回 Yes No No
所有tokens不暴露在浏览器 Yes No No
能够验证客户端密钥 Yes No Yes
能够使用刷新令牌 Yes No Yes
仅需一次请求 No Yes No
大部分请求由后端进行 Yes No 可变

支持返回类型对比

返回类型 认证模式 说明
code Authorization Code Flow 仅返回授权码
id_token Implicit Flow 返回身份令牌
id_token token Implicit Flow 返回身份令牌、通行令牌
code id_token Hybrid Flow 返回授权码、身份令牌
code token Hybrid Flow 返回授权码、通行令牌
code id_token token Hybrid Flow 返回授权码、身份令牌、通行令牌

授权码模式解析

相对来说,授权码模式还是用的最多的,我们详细解读一下本模式的协议内容。

授权时序图

sequenceDiagram 用户->>客户端: 请求受保护资源 客户端->>认证服务: 准备入参,发起认证请求 认证服务->>认证服务: 认证用户 认证服务->>用户: 是否同意授权 认证服务->>客户端: 发放授权码(前端进行) 客户端->>认证服务: 使用授权码请求token(后端进行) 认证服务->>认证服务: 校验客户端密钥,校验授权码 认证服务->>客户端: 发放身份令牌、通行令牌(后端进行) 客户端->>客户端: 校验身份令牌,获取用户标识

认证请求

认证接口必须同时支持GET和POST两种请求方式。如果使用GET方法,客户端必须使用URI Query传递参数,如果使用POST方法,客户端必须使用Form传递参数。

参数定义

请求报文示例

HTTP/1.1 302 Found
  Location: https://server.example.com/authorize?
    response_type=code
    &scope=openid%20profile%20email
    &client_id=s6BhdRkqt3
    &state=af0ifjsldkj
    &redirect_uri=https%3A%2F%2Fclient.example.org%2Fcb

认证请求校验

认证终端用户

认证服务必须想办法防止过程中的跨站伪造攻击和点击劫持攻击。

获取终端用户授权/同意

终端用户通过认证之后,认证服务必须与终端用户交互,询问用户是否同意对客户端的授权。

认证响应

成功响应

使用 application/x-www-form-urlencoded格式返回结果
例如:

 HTTP/1.1 302 Found
  Location: https://client.example.org/cb?
    code=SplxlOBeZQQYbYS6WxSbIA
    &state=af0ifjsldkj

失败响应

错误代码包括这些
oauth2.0定义的响应代码

例如:

  HTTP/1.1 302 Found
  Location: https://client.example.org/cb?
    error=invalid_request
    &error_description=
      Unsupported%20response_type%20value
    &state=af0ifjsldkj

客户端校验授权码

协议规定客户端必须校验授权码的正确性

源码解析

从AuthorizeEndpoint的ProcessAsync方法作为入口开始认证接口的源码解析。

    public override async Task<IEndpointResult> ProcessAsync(HttpContext context)
        {
            Logger.LogDebug("Start authorize request");

            NameValueCollection values;

            if (HttpMethods.IsGet(context.Request.Method))
            {
                values = context.Request.Query.AsNameValueCollection();
            }
            else if (HttpMethods.IsPost(context.Request.Method))
            {
                if (!context.Request.HasFormContentType)
                {
                    return new StatusCodeResult(HttpStatusCode.UnsupportedMediaType);
                }

                values = context.Request.Form.AsNameValueCollection();
            }
            else
            {
                return new StatusCodeResult(HttpStatusCode.MethodNotAllowed);
            }

            var user = await UserSession.GetUserAsync();
            var result = await ProcessAuthorizeRequestAsync(values, user, null);

            Logger.LogTrace("End authorize request. result type: {0}", result?.GetType().ToString() ?? "-none-");

            return result;
        }

认证站点如果cookie中存在当前会话信息,则直接返回用户信息,否则调用cookie架构的认证方法,会跳转到登录页面。

public virtual async Task<ClaimsPrincipal> GetUserAsync()
{
    await AuthenticateAsync();

    return Principal;
}

protected virtual async Task AuthenticateAsync()
    {
        if (Principal == null || Properties == null)
        {
            var scheme = await GetCookieSchemeAsync();

            var handler = await Handlers.GetHandlerAsync(HttpContext, scheme);
            if (handler == null)
            {
                throw new InvalidOperationException($"No authentication handler is configured to authenticate for the scheme: {scheme}");
            }

            var result = await handler.AuthenticateAsync();
            if (result != null && result.Succeeded)
            {
                Principal = result.Principal;
                Properties = result.Properties;
            }
        }
    }

认证请求处理流程大致分为三步

 internal async Task<IEndpointResult> ProcessAuthorizeRequestAsync(NameValueCollection parameters, ClaimsPrincipal user, ConsentResponse consent)
{
    if (user != null)
    {
        Logger.LogDebug("User in authorize request: {subjectId}", user.GetSubjectId());
    }
    else
    {
        Logger.LogDebug("No user present in authorize request");
    }

    // validate request
    var result = await _validator.ValidateAsync(parameters, user);
    if (result.IsError)
    {
        return await CreateErrorResultAsync(
            "Request validation failed",
            result.ValidatedRequest,
            result.Error,
            result.ErrorDescription);
    }

    var request = result.ValidatedRequest;
    LogRequest(request);

    // determine user interaction
    var interactionResult = await _interactionGenerator.ProcessInteractionAsync(request, consent);
    if (interactionResult.IsError)
    {
        return await CreateErrorResultAsync("Interaction generator error", request, interactionResult.Error, interactionResult.ErrorDescription, false);
    }
    if (interactionResult.IsLogin)
    {
        return new LoginPageResult(request);
    }
    if (interactionResult.IsConsent)
    {
        return new ConsentPageResult(request);
    }
    if (interactionResult.IsRedirect)
    {
        return new CustomRedirectResult(request, interactionResult.RedirectUrl);
    }

    var response = await _authorizeResponseGenerator.CreateResponseAsync(request);

    await RaiseResponseEventAsync(response);

    LogResponse(response);

    return new AuthorizeResult(response);
}

生成返回信息

此处只有AuthorizationCode、Implicit、Hybrid三种授权类型的判断,用户名密码、客户端密钥模式不能使用authorize接口。

  public virtual async Task<AuthorizeResponse> CreateResponseAsync(ValidatedAuthorizeRequest request)
{
    if (request.GrantType == GrantType.AuthorizationCode)
    {
        return await CreateCodeFlowResponseAsync(request);
    }
    if (request.GrantType == GrantType.Implicit)
    {
        return await CreateImplicitFlowResponseAsync(request);
    }
    if (request.GrantType == GrantType.Hybrid)
    {
        return await CreateHybridFlowResponseAsync(request);
    }

    Logger.LogError("Unsupported grant type: " + request.GrantType);
    throw new InvalidOperationException("invalid grant type: " + request.GrantType);
}
 protected virtual async Task<AuthorizeResponse> CreateCodeFlowResponseAsync(ValidatedAuthorizeRequest request)
{
    Logger.LogDebug("Creating Authorization Code Flow response.");

    var code = await CreateCodeAsync(request);
    var id = await AuthorizationCodeStore.StoreAuthorizationCodeAsync(code);

    var response = new AuthorizeResponse
    {
        Request = request,
        Code = id,
        SessionState = request.GenerateSessionStateValue()
    };

    return response;
}

protected virtual async Task<AuthorizationCode> CreateCodeAsync(ValidatedAuthorizeRequest request)
    {
        string stateHash = null;
        if (request.State.IsPresent())
        {
            var credential = await KeyMaterialService.GetSigningCredentialsAsync();
            if (credential == null)
            {
                throw new InvalidOperationException("No signing credential is configured.");
            }

            var algorithm = credential.Algorithm;
            stateHash = CryptoHelper.CreateHashClaimValue(request.State, algorithm);
        }

        var code = new AuthorizationCode
        {
            CreationTime = Clock.UtcNow.UtcDateTime,
            ClientId = request.Client.ClientId,
            Lifetime = request.Client.AuthorizationCodeLifetime,
            Subject = request.Subject,
            SessionId = request.SessionId,
            CodeChallenge = request.CodeChallenge.Sha256(),
            CodeChallengeMethod = request.CodeChallengeMethod,

            IsOpenId = request.IsOpenIdRequest,
            RequestedScopes = request.ValidatedScopes.GrantedResources.ToScopeNames(),
            RedirectUri = request.RedirectUri,
            Nonce = request.Nonce,
            StateHash = stateHash,

            WasConsentShown = request.WasConsentShown
        };

        return code;
    }

返回结果

302 https://mysite.com?code=xxxxx&state=xxx
<html>
<head>
    <meta http-equiv='X-UA-Compatible' content='IE=edge' />
    <base target='_self'/>
</head>
<body>
    <form method='post' action='https://mysite.com'>
        <input type='hidden' name='code' value='xxx' />
        <input type='hidden' name='state' value='xxx' />
        <noscript>
            <button>Click to continue</button>
        </noscript>
    </form>
    <script>window.addEventListener('load', function(){document.forms[0].submit();});</script>
</body>
</html>
private async Task RenderAuthorizeResponseAsync(HttpContext context)
{
    if (Response.Request.ResponseMode == OidcConstants.ResponseModes.Query ||
        Response.Request.ResponseMode == OidcConstants.ResponseModes.Fragment)
    {
        context.Response.SetNoCache();
        context.Response.Redirect(BuildRedirectUri());
    }
    else if (Response.Request.ResponseMode == OidcConstants.ResponseModes.FormPost)
    {
        context.Response.SetNoCache();
        AddSecurityHeaders(context);
        await context.Response.WriteHtmlAsync(GetFormPostHtml());
    }
    else
    {
        //_logger.LogError("Unsupported response mode.");
        throw new InvalidOperationException("Unsupported response mode");
    }
}

客户端在回调地址接收code,即可向token接口换取token。

其他

简单看一下简化流程和混合流程是怎么创建返回报文的。

简化流程生成返回报文

可以看到,简化流程的所有token都是由authorization接口返回的,一次请求返回所有token。

protected virtual async Task<AuthorizeResponse> CreateImplicitFlowResponseAsync(ValidatedAuthorizeRequest request, string authorizationCode = null)
    {
        Logger.LogDebug("Creating Implicit Flow response.");

        string accessTokenValue = null;
        int accessTokenLifetime = 0;

        var responseTypes = request.ResponseType.FromSpaceSeparatedString();

        if (responseTypes.Contains(OidcConstants.ResponseTypes.Token))
        {
            var tokenRequest = new TokenCreationRequest
            {
                Subject = request.Subject,
                Resources = request.ValidatedScopes.GrantedResources,

                ValidatedRequest = request
            };

            var accessToken = await TokenService.CreateAccessTokenAsync(tokenRequest);
            accessTokenLifetime = accessToken.Lifetime;

            accessTokenValue = await TokenService.CreateSecurityTokenAsync(accessToken);
        }

        string jwt = null;
        if (responseTypes.Contains(OidcConstants.ResponseTypes.IdToken))
        {
            string stateHash = null;
            if (request.State.IsPresent())
            {
                var credential = await KeyMaterialService.GetSigningCredentialsAsync();
                if (credential == null)
                {
                    throw new InvalidOperationException("No signing credential is configured.");
                }

                var algorithm = credential.Algorithm;
                stateHash = CryptoHelper.CreateHashClaimValue(request.State, algorithm);
            }

            var tokenRequest = new TokenCreationRequest
            {
                ValidatedRequest = request,
                Subject = request.Subject,
                Resources = request.ValidatedScopes.GrantedResources,
                Nonce = request.Raw.Get(OidcConstants.AuthorizeRequest.Nonce),
                IncludeAllIdentityClaims = !request.AccessTokenRequested,
                AccessTokenToHash = accessTokenValue,
                AuthorizationCodeToHash = authorizationCode,
                StateHash = stateHash
            };

            var idToken = await TokenService.CreateIdentityTokenAsync(tokenRequest);
            jwt = await TokenService.CreateSecurityTokenAsync(idToken);
        }

        var response = new AuthorizeResponse
        {
            Request = request,
            AccessToken = accessTokenValue,
            AccessTokenLifetime = accessTokenLifetime,
            IdentityToken = jwt,
            SessionState = request.GenerateSessionStateValue()
        };

        return response;
    }

混合流程生成返回报文

这段代码充分体现了它为啥叫混合流程,把生成授权码的方法调一遍,再把简化流程的方法调一遍,code和token可以一起返回。

protected virtual async Task<AuthorizeResponse> CreateHybridFlowResponseAsync(ValidatedAuthorizeRequest request)
    {
        Logger.LogDebug("Creating Hybrid Flow response.");

        var code = await CreateCodeAsync(request);
        var id = await AuthorizationCodeStore.StoreAuthorizationCodeAsync(code);

        var response = await CreateImplicitFlowResponseAsync(request, id);
        response.Code = id;

        return response;
    }