예제 #1
0
        private async Task <bool> InvokeTokenEndpointAsync()
        {
            if (!string.Equals(Request.Method, "POST", StringComparison.OrdinalIgnoreCase))
            {
                Logger.LogError("The token request was rejected because an invalid " +
                                "HTTP method was received: {Method}.", Request.Method);

                return(await SendErrorPayloadAsync(new OpenIdConnectMessage {
                    Error = OpenIdConnectConstants.Errors.InvalidRequest,
                    ErrorDescription = "A malformed token request has been received: make sure to use POST."
                }));
            }

            // See http://openid.net/specs/openid-connect-core-1_0.html#FormSerialization
            if (string.IsNullOrEmpty(Request.ContentType))
            {
                Logger.LogError("The token request was rejected because the " +
                                "mandatory 'Content-Type' header was missing.");

                return(await SendErrorPayloadAsync(new OpenIdConnectMessage {
                    Error = OpenIdConnectConstants.Errors.InvalidRequest,
                    ErrorDescription = "A malformed token request has been received: " +
                                       "the mandatory 'Content-Type' header was missing from the POST request."
                }));
            }

            // May have media/type; charset=utf-8, allow partial match.
            if (!Request.ContentType.StartsWith("application/x-www-form-urlencoded", StringComparison.OrdinalIgnoreCase))
            {
                Logger.LogError("The token request was rejected because an invalid 'Content-Type' " +
                                "header was received: {ContentType}.", Request.ContentType);

                return(await SendErrorPayloadAsync(new OpenIdConnectMessage {
                    Error = OpenIdConnectConstants.Errors.InvalidRequest,
                    ErrorDescription = "A malformed token request has been received: " +
                                       "the 'Content-Type' header contained an unexcepted value. " +
                                       "Make sure to use 'application/x-www-form-urlencoded'."
                }));
            }

            var form = await Request.ReadFormAsync(Context.RequestAborted);

            var request = new OpenIdConnectMessage(form.ToDictionary())
            {
                RequestType = OpenIdConnectRequestType.TokenRequest
            };

            // Store the token request in the ASP.NET context.
            Context.SetOpenIdConnectRequest(request);

            // Reject token requests missing the mandatory grant_type parameter.
            if (string.IsNullOrEmpty(request.GrantType))
            {
                Logger.LogError("The token request was rejected because the grant type was missing.");

                return(await SendErrorPayloadAsync(new OpenIdConnectMessage {
                    Error = OpenIdConnectConstants.Errors.InvalidRequest,
                    ErrorDescription = "The mandatory 'grant_type' parameter was missing.",
                }));
            }

            // Reject grant_type=authorization_code requests missing the authorization code.
            // See https://tools.ietf.org/html/rfc6749#section-4.1.3
            else if (request.IsAuthorizationCodeGrantType() && string.IsNullOrEmpty(request.Code))
            {
                Logger.LogError("The token request was rejected because the authorization code was missing.");

                return(await SendErrorPayloadAsync(new OpenIdConnectMessage {
                    Error = OpenIdConnectConstants.Errors.InvalidRequest,
                    ErrorDescription = "The mandatory 'code' parameter was missing."
                }));
            }

            // Reject grant_type=refresh_token requests missing the refresh token.
            // See https://tools.ietf.org/html/rfc6749#section-6
            else if (request.IsRefreshTokenGrantType() && string.IsNullOrEmpty(request.RefreshToken))
            {
                Logger.LogError("The token request was rejected because the refresh token was missing.");

                return(await SendErrorPayloadAsync(new OpenIdConnectMessage {
                    Error = OpenIdConnectConstants.Errors.InvalidRequest,
                    ErrorDescription = "The mandatory 'refresh_token' parameter was missing."
                }));
            }

            // Reject grant_type=password requests missing username or password.
            // See https://tools.ietf.org/html/rfc6749#section-4.3.2
            else if (request.IsPasswordGrantType() && (string.IsNullOrEmpty(request.Username) ||
                                                       string.IsNullOrEmpty(request.Password)))
            {
                Logger.LogError("The token request was rejected because the resource owner credentials were missing.");

                return(await SendErrorPayloadAsync(new OpenIdConnectMessage {
                    Error = OpenIdConnectConstants.Errors.InvalidRequest,
                    ErrorDescription = "The mandatory 'username' and/or 'password' parameters " +
                                       "was/were missing from the request message."
                }));
            }

            // When client_id and client_secret are both null, try to extract them from the Authorization header.
            // See http://tools.ietf.org/html/rfc6749#section-2.3.1 and
            // http://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication
            if (string.IsNullOrEmpty(request.ClientId) && string.IsNullOrEmpty(request.ClientSecret))
            {
                string header = Request.Headers[HeaderNames.Authorization];
                if (!string.IsNullOrEmpty(header) && header.StartsWith("Basic ", StringComparison.OrdinalIgnoreCase))
                {
                    try {
                        var value = header.Substring("Basic ".Length).Trim();
                        var data  = Encoding.UTF8.GetString(Convert.FromBase64String(value));

                        var index = data.IndexOf(':');
                        if (index >= 0)
                        {
                            request.ClientId     = data.Substring(0, index);
                            request.ClientSecret = data.Substring(index + 1);
                        }
                    }

                    catch (FormatException) { }
                    catch (ArgumentException) { }
                }
            }

            var validatingContext = new ValidateTokenRequestContext(Context, Options, request);
            await Options.Provider.ValidateTokenRequest(validatingContext);

            if (validatingContext.IsRejected)
            {
                Logger.LogInformation("The token request was rejected by application code.");

                return(await SendErrorPayloadAsync(new OpenIdConnectMessage {
                    Error = validatingContext.Error ?? OpenIdConnectConstants.Errors.InvalidClient,
                    ErrorDescription = validatingContext.ErrorDescription,
                    ErrorUri = validatingContext.ErrorUri
                }));
            }

            // Reject grant_type=client_credentials requests if validation was skipped.
            else if (validatingContext.IsSkipped && request.IsClientCredentialsGrantType())
            {
                Logger.LogError("The token request must be fully validated to use the client_credentials grant type.");

                return(await SendErrorPayloadAsync(new OpenIdConnectMessage {
                    Error = OpenIdConnectConstants.Errors.InvalidGrant,
                    ErrorDescription = "Client authentication is required when using client_credentials."
                }));
            }

            // Ensure that the client_id has been set from the ValidateTokenRequest event.
            else if (validatingContext.IsValidated && string.IsNullOrEmpty(request.ClientId))
            {
                Logger.LogError("The token request was validated but the client_id was not set.");

                return(await SendErrorPayloadAsync(new OpenIdConnectMessage {
                    Error = OpenIdConnectConstants.Errors.ServerError,
                    ErrorDescription = "An internal server error occurred."
                }));
            }

            AuthenticationTicket ticket = null;

            // See http://tools.ietf.org/html/rfc6749#section-4.1
            // and http://tools.ietf.org/html/rfc6749#section-4.1.3 (authorization code grant).
            // See http://tools.ietf.org/html/rfc6749#section-6 (refresh token grant).
            if (request.IsAuthorizationCodeGrantType() || request.IsRefreshTokenGrantType())
            {
                ticket = request.IsAuthorizationCodeGrantType() ?
                         await DeserializeAuthorizationCodeAsync(request.Code, request) :
                         await DeserializeRefreshTokenAsync(request.RefreshToken, request);

                if (ticket == null)
                {
                    Logger.LogError("The token request was rejected because the " +
                                    "authorization code or the refresh token was invalid.");

                    return(await SendErrorPayloadAsync(new OpenIdConnectMessage {
                        Error = OpenIdConnectConstants.Errors.InvalidGrant,
                        ErrorDescription = "Invalid ticket"
                    }));
                }

                if (!ticket.Properties.ExpiresUtc.HasValue ||
                    ticket.Properties.ExpiresUtc < Options.SystemClock.UtcNow)
                {
                    Logger.LogError("The token request was rejected because the " +
                                    "authorization code or the refresh token was expired.");

                    return(await SendErrorPayloadAsync(new OpenIdConnectMessage {
                        Error = OpenIdConnectConstants.Errors.InvalidGrant,
                        ErrorDescription = "Expired ticket"
                    }));
                }

                // If the client was fully authenticated when retrieving its refresh token,
                // the current request must be rejected if client authentication was not enforced.
                if (request.IsRefreshTokenGrantType() && !validatingContext.IsValidated && ticket.IsConfidential())
                {
                    Logger.LogError("The token request was rejected because client authentication " +
                                    "was required to use the confidential refresh token.");

                    return(await SendErrorPayloadAsync(new OpenIdConnectMessage {
                        Error = OpenIdConnectConstants.Errors.InvalidGrant,
                        ErrorDescription = "Client authentication is required to use this ticket"
                    }));
                }

                // Note: presenters may be empty during a grant_type=refresh_token request if the refresh token
                // was issued to a public client but cannot be null for an authorization code grant request.
                var presenters = ticket.GetPresenters();
                if (request.IsAuthorizationCodeGrantType() && !presenters.Any())
                {
                    Logger.LogError("The token request was rejected because the authorization " +
                                    "code didn't contain any valid presenter.");

                    return(await SendErrorPayloadAsync(new OpenIdConnectMessage {
                        Error = OpenIdConnectConstants.Errors.ServerError,
                        ErrorDescription = "An internal server error occurred."
                    }));
                }

                // At this stage, client_id cannot be null for grant_type=authorization_code requests,
                // as it must either be set in the ValidateClientAuthentication notification
                // by the developer or manually flowed by non-confidential client applications.
                // See https://tools.ietf.org/html/rfc6749#section-4.1.3
                if (request.IsAuthorizationCodeGrantType() && string.IsNullOrEmpty(request.ClientId))
                {
                    Logger.LogError("The token request was rejected because the mandatory 'client_id' was missing.");

                    return(await SendErrorPayloadAsync(new OpenIdConnectMessage {
                        Error = OpenIdConnectConstants.Errors.InvalidRequest,
                        ErrorDescription = "client_id was missing from the token request"
                    }));
                }

                // Ensure the authorization code/refresh token was issued to the client application making the token request.
                // Note: when using the refresh token grant, client_id is optional but must validated if present.
                // As a consequence, this check doesn't depend on the actual status of client authentication.
                // See https://tools.ietf.org/html/rfc6749#section-6
                // and http://openid.net/specs/openid-connect-core-1_0.html#RefreshingAccessToken
                if (!string.IsNullOrEmpty(request.ClientId) && presenters.Any() &&
                    !presenters.Contains(request.ClientId, StringComparer.Ordinal))
                {
                    Logger.LogError("The token request was rejected because the authorization " +
                                    "code was issued to a different client application.");

                    return(await SendErrorPayloadAsync(new OpenIdConnectMessage {
                        Error = OpenIdConnectConstants.Errors.InvalidGrant,
                        ErrorDescription = "Ticket does not contain matching client_id"
                    }));
                }

                // Validate the redirect_uri flowed by the client application during this token request.
                // Note: for pure OAuth2 requests, redirect_uri is only mandatory if the authorization request
                // contained an explicit redirect_uri. OpenID Connect requests MUST include a redirect_uri
                // but the specifications allow proceeding the token request without returning an error
                // if the authorization request didn't contain an explicit redirect_uri.
                // See https://tools.ietf.org/html/rfc6749#section-4.1.3
                // and http://openid.net/specs/openid-connect-core-1_0.html#TokenRequestValidation
                var address = ticket.GetProperty(OpenIdConnectConstants.Properties.RedirectUri);
                if (request.IsAuthorizationCodeGrantType() && !string.IsNullOrEmpty(address))
                {
                    ticket.Properties.Items.Remove(OpenIdConnectConstants.Properties.RedirectUri);

                    if (string.IsNullOrEmpty(request.RedirectUri))
                    {
                        Logger.LogError("The token request was rejected because the mandatory 'redirect_uri' " +
                                        "parameter was missing from the grant_type=authorization_code request.");

                        return(await SendErrorPayloadAsync(new OpenIdConnectMessage {
                            Error = OpenIdConnectConstants.Errors.InvalidRequest,
                            ErrorDescription = "redirect_uri was missing from the token request"
                        }));
                    }

                    else if (!string.Equals(address, request.RedirectUri, StringComparison.Ordinal))
                    {
                        Logger.LogError("The token request was rejected because the 'redirect_uri' " +
                                        "parameter didn't correspond to the expected value.");

                        return(await SendErrorPayloadAsync(new OpenIdConnectMessage {
                            Error = OpenIdConnectConstants.Errors.InvalidGrant,
                            ErrorDescription = "Authorization code does not contain matching redirect_uri"
                        }));
                    }
                }

                if (!string.IsNullOrEmpty(request.Scope))
                {
                    // When an explicit scope parameter has been included in the token request
                    // but was missing from the authorization request, the request MUST be rejected.
                    // See http://tools.ietf.org/html/rfc6749#section-6
                    var scopes = ticket.GetScopes();
                    if (!scopes.Any())
                    {
                        Logger.LogError("The token request was rejected because the 'scope' parameter was not allowed.");

                        return(await SendErrorPayloadAsync(new OpenIdConnectMessage {
                            Error = OpenIdConnectConstants.Errors.InvalidGrant,
                            ErrorDescription = "Token request cannot contain a scope parameter" +
                                               "if the authorization request didn't contain one"
                        }));
                    }

                    // When an explicit scope parameter has been included in the token request,
                    // the authorization server MUST ensure that it doesn't contain scopes
                    // that were not allowed during the authorization request.
                    // See https://tools.ietf.org/html/rfc6749#section-6
                    else if (!new HashSet <string>(scopes).IsSupersetOf(request.GetScopes()))
                    {
                        Logger.LogError("The token request was rejected because the 'scope' parameter was not valid.");

                        return(await SendErrorPayloadAsync(new OpenIdConnectMessage {
                            Error = OpenIdConnectConstants.Errors.InvalidGrant,
                            ErrorDescription = "Token request doesn't contain a valid scope parameter"
                        }));
                    }

                    // Replace the scopes initially granted by the scopes
                    // listed by the client application in the token request.
                    ticket.SetScopes(request.GetScopes());
                }

                if (request.IsAuthorizationCodeGrantType())
                {
                    // Note: the authentication ticket is copied to avoid modifying the properties of the authorization code.
                    var context = new GrantAuthorizationCodeContext(Context, Options, request, ticket.Copy());
                    await Options.Provider.GrantAuthorizationCode(context);

                    if (!context.IsValidated)
                    {
                        // Note: use invalid_grant as the default error if none has been explicitly provided.
                        return(await SendErrorPayloadAsync(new OpenIdConnectMessage {
                            Error = context.Error ?? OpenIdConnectConstants.Errors.InvalidGrant,
                            ErrorDescription = context.ErrorDescription,
                            ErrorUri = context.ErrorUri
                        }));
                    }

                    // By default, when using the authorization code grant, the authentication ticket extracted from the
                    // authorization code is used as-is. To avoid aligning the expiration date of the generated tokens
                    // with the lifetime of the authorization code, the ticket properties are automatically reset to null.
                    if (context.Ticket.Properties.IssuedUtc == ticket.Properties.IssuedUtc)
                    {
                        context.Ticket.Properties.IssuedUtc = null;
                    }

                    if (context.Ticket.Properties.ExpiresUtc == ticket.Properties.ExpiresUtc)
                    {
                        context.Ticket.Properties.ExpiresUtc = null;
                    }

                    ticket = context.Ticket;
                }

                else
                {
                    // Note: the authentication ticket is copied to avoid modifying the properties of the refresh token.
                    var context = new GrantRefreshTokenContext(Context, Options, request, ticket.Copy());
                    await Options.Provider.GrantRefreshToken(context);

                    if (!context.IsValidated)
                    {
                        // Note: use invalid_grant as the default error if none has been explicitly provided.
                        return(await SendErrorPayloadAsync(new OpenIdConnectMessage {
                            Error = context.Error ?? OpenIdConnectConstants.Errors.InvalidGrant,
                            ErrorDescription = context.ErrorDescription,
                            ErrorUri = context.ErrorUri
                        }));
                    }

                    // By default, when using the refresh token grant, the authentication ticket extracted from the
                    // refresh token is used as-is. To avoid aligning the expiration date of the generated tokens
                    // with the lifetime of the refresh token, the ticket properties are automatically reset to null.
                    if (context.Ticket.Properties.IssuedUtc == ticket.Properties.IssuedUtc)
                    {
                        context.Ticket.Properties.IssuedUtc = null;
                    }

                    if (context.Ticket.Properties.ExpiresUtc == ticket.Properties.ExpiresUtc)
                    {
                        context.Ticket.Properties.ExpiresUtc = null;
                    }

                    ticket = context.Ticket;
                }
            }

            // See http://tools.ietf.org/html/rfc6749#section-4.3
            // and http://tools.ietf.org/html/rfc6749#section-4.3.2
            else if (request.IsPasswordGrantType())
            {
                var context = new GrantResourceOwnerCredentialsContext(Context, Options, request);
                await Options.Provider.GrantResourceOwnerCredentials(context);

                if (!context.IsValidated)
                {
                    // Note: use invalid_grant as the default error if none has been explicitly provided.
                    return(await SendErrorPayloadAsync(new OpenIdConnectMessage {
                        Error = context.Error ?? OpenIdConnectConstants.Errors.InvalidGrant,
                        ErrorDescription = context.ErrorDescription,
                        ErrorUri = context.ErrorUri
                    }));
                }

                ticket = context.Ticket;
            }

            // See http://tools.ietf.org/html/rfc6749#section-4.4
            // and http://tools.ietf.org/html/rfc6749#section-4.4.2
            else if (request.IsClientCredentialsGrantType())
            {
                var context = new GrantClientCredentialsContext(Context, Options, request);
                await Options.Provider.GrantClientCredentials(context);

                if (!context.IsValidated)
                {
                    // Note: use unauthorized_client as the default error if none has been explicitly provided.
                    return(await SendErrorPayloadAsync(new OpenIdConnectMessage {
                        Error = context.Error ?? OpenIdConnectConstants.Errors.UnauthorizedClient,
                        ErrorDescription = context.ErrorDescription,
                        ErrorUri = context.ErrorUri
                    }));
                }

                ticket = context.Ticket;
            }

            // See http://tools.ietf.org/html/rfc6749#section-8.3
            else
            {
                var context = new GrantCustomExtensionContext(Context, Options, request);
                await Options.Provider.GrantCustomExtension(context);

                if (!context.IsValidated)
                {
                    // Note: use unsupported_grant_type as the default error if none has been explicitly provided.
                    return(await SendErrorPayloadAsync(new OpenIdConnectMessage {
                        Error = context.Error ?? OpenIdConnectConstants.Errors.UnsupportedGrantType,
                        ErrorDescription = context.ErrorDescription,
                        ErrorUri = context.ErrorUri
                    }));
                }

                ticket = context.Ticket;
            }

            var notification = new HandleTokenRequestContext(Context, Options, request, ticket);
            await Options.Provider.HandleTokenRequest(notification);

            if (notification.HandledResponse)
            {
                return(true);
            }

            else if (notification.Skipped)
            {
                return(false);
            }

            // Flow the changes made to the ticket.
            ticket = notification.Ticket;

            // Ensure an authentication ticket has been provided:
            // a null ticket MUST result in an internal server error.
            if (ticket == null)
            {
                Logger.LogError("The token request was rejected because no authentication " +
                                "ticket was returned by application code.");

                return(await SendErrorPayloadAsync(new OpenIdConnectMessage {
                    Error = OpenIdConnectConstants.Errors.ServerError
                }));
            }

            if (validatingContext.IsValidated)
            {
                // Store a boolean indicating whether the ticket should be marked as confidential.
                ticket.Properties.Items[OpenIdConnectConstants.Properties.Confidential] = "true";
            }

            // Always include the "openid" scope when the developer doesn't explicitly call SetScopes.
            // Note: the application is allowed to specify a different "scopes": in this case,
            // don't replace the "scopes" property stored in the authentication ticket.
            if (!ticket.Properties.Items.ContainsKey(OpenIdConnectConstants.Properties.Scopes) &&
                request.HasScope(OpenIdConnectConstants.Scopes.OpenId))
            {
                ticket.Properties.Items[OpenIdConnectConstants.Properties.Scopes] = OpenIdConnectConstants.Scopes.OpenId;
            }

            string audiences;

            // When a "resources" property cannot be found in the authentication properties, infer it from the "audiences" property.
            if (!ticket.Properties.Items.ContainsKey(OpenIdConnectConstants.Properties.Resources) &&
                ticket.Properties.Items.TryGetValue(OpenIdConnectConstants.Properties.Audiences, out audiences))
            {
                ticket.Properties.Items[OpenIdConnectConstants.Properties.Resources] = audiences;
            }

            var response = new OpenIdConnectMessage();

            // Note: by default, an access token is always returned, but the client application can use the "response_type" parameter
            // to only include specific types of tokens. When this parameter is missing, an access token is always generated.
            if (string.IsNullOrEmpty(request.ResponseType) || request.HasResponseType(OpenIdConnectConstants.ResponseTypes.Token))
            {
                // Make sure to create a copy of the authentication properties
                // to avoid modifying the properties set on the original ticket.
                var properties = ticket.Properties.Copy();

                string resources;
                if (!properties.Items.TryGetValue(OpenIdConnectConstants.Properties.Resources, out resources))
                {
                    Logger.LogInformation("No explicit resource has been associated with the authentication ticket: " +
                                          "the access token will thus be issued without any audience attached.");
                }

                // Note: when the "resource" parameter added to the OpenID Connect response
                // is identical to the request parameter, keeping it is not necessary.
                if (request.IsAuthorizationCodeGrantType() || (!string.IsNullOrEmpty(request.Resource) &&
                                                               !string.Equals(request.Resource, resources, StringComparison.Ordinal)))
                {
                    response.Resource = resources;
                }

                // Note: when the "scope" parameter added to the OpenID Connect response
                // is identical to the request parameter, keeping it is not necessary.
                string scopes;
                properties.Items.TryGetValue(OpenIdConnectConstants.Properties.Scopes, out scopes);
                if (request.IsAuthorizationCodeGrantType() || (!string.IsNullOrEmpty(request.Scope) &&
                                                               !string.Equals(request.Scope, scopes, StringComparison.Ordinal)))
                {
                    response.Scope = scopes;
                }

                response.TokenType   = OpenIdConnectConstants.TokenTypes.Bearer;
                response.AccessToken = await SerializeAccessTokenAsync(ticket.Principal, properties, request, response);

                // Ensure that an access token is issued to avoid returning an invalid response.
                // See http://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#Combinations
                if (string.IsNullOrEmpty(response.AccessToken))
                {
                    Logger.LogError("An error occurred during the serialization of the " +
                                    "access token and a null value was returned.");

                    return(await SendErrorPayloadAsync(new OpenIdConnectMessage {
                        Error = OpenIdConnectConstants.Errors.ServerError,
                        ErrorDescription = "no valid access token was issued"
                    }));
                }

                // properties.ExpiresUtc is automatically set by SerializeAccessTokenAsync but the end user
                // is free to set a null value directly in the SerializeAccessToken event.
                if (properties.ExpiresUtc.HasValue && properties.ExpiresUtc > Options.SystemClock.UtcNow)
                {
                    var lifetime   = properties.ExpiresUtc.Value - Options.SystemClock.UtcNow;
                    var expiration = (long)(lifetime.TotalSeconds + .5);

                    response.ExpiresIn = expiration.ToString(CultureInfo.InvariantCulture);
                }
            }

            // Note: by default, an identity token is always returned when the "openid" scope has been requested,
            // but the client application can use the "response_type" parameter to only include specific types of tokens.
            // When this parameter is missing, an identity token is always generated.
            if (ticket.HasScope(OpenIdConnectConstants.Scopes.OpenId) &&
                (string.IsNullOrEmpty(request.ResponseType) || request.HasResponseType(OpenIdConnectConstants.ResponseTypes.IdToken)))
            {
                // Make sure to create a copy of the authentication properties
                // to avoid modifying the properties set on the original ticket.
                var properties = ticket.Properties.Copy();

                response.IdToken = await SerializeIdentityTokenAsync(ticket.Principal, properties, request, response);

                // Ensure that an identity token is issued to avoid returning an invalid response.
                // See http://openid.net/specs/openid-connect-core-1_0.html#TokenResponse
                // and http://openid.net/specs/openid-connect-core-1_0.html#RefreshTokenResponse
                if (string.IsNullOrEmpty(response.IdToken))
                {
                    Logger.LogError("An error occurred during the serialization of the " +
                                    "identity token and a null value was returned.");

                    return(await SendErrorPayloadAsync(new OpenIdConnectMessage {
                        Error = OpenIdConnectConstants.Errors.ServerError,
                        ErrorDescription = "no valid identity token was issued"
                    }));
                }
            }

            // Note: by default, a refresh token is always returned when the "offline_access" scope has been requested,
            // but the client application can use the "response_type" parameter to only include specific types of tokens.
            // When this parameter is missing, a refresh token is always generated.
            if (ticket.HasScope(OpenIdConnectConstants.Scopes.OfflineAccess) &&
                (!request.IsRefreshTokenGrantType() || Options.UseSlidingExpiration) &&
                (string.IsNullOrEmpty(request.ResponseType) || request.HasResponseType(OpenIdConnectConstants.Parameters.RefreshToken)))
            {
                // Make sure to create a copy of the authentication properties
                // to avoid modifying the properties set on the original ticket.
                var properties = ticket.Properties.Copy();

                response.RefreshToken = await SerializeRefreshTokenAsync(ticket.Principal, properties, request, response);
            }

            var payload = new JObject();

            foreach (var parameter in response.Parameters)
            {
                payload.Add(parameter.Key, parameter.Value);
            }

            var responseNotification = new ApplyTokenResponseContext(Context, Options, ticket, request, payload);
            await Options.Provider.ApplyTokenResponse(responseNotification);

            if (responseNotification.HandledResponse)
            {
                return(true);
            }

            else if (responseNotification.Skipped)
            {
                return(false);
            }

            using (var buffer = new MemoryStream())
                using (var writer = new JsonTextWriter(new StreamWriter(buffer))) {
                    payload.WriteTo(writer);
                    writer.Flush();

                    Response.ContentLength = buffer.Length;
                    Response.ContentType   = "application/json;charset=UTF-8";

                    Response.Headers[HeaderNames.CacheControl] = "no-cache";
                    Response.Headers[HeaderNames.Pragma]       = "no-cache";
                    Response.Headers[HeaderNames.Expires]      = "-1";

                    buffer.Seek(offset: 0, loc: SeekOrigin.Begin);
                    await buffer.CopyToAsync(Response.Body, 4096, Context.RequestAborted);

                    return(true);
                }
        }
예제 #2
0
        private async Task <bool> InvokeTokenEndpointAsync()
        {
            if (!string.Equals(Request.Method, "POST", StringComparison.OrdinalIgnoreCase))
            {
                Logger.LogError("The token request was rejected because an invalid " +
                                "HTTP method was received: {Method}.", Request.Method);

                return(await SendTokenResponseAsync(new OpenIdConnectResponse
                {
                    Error = OpenIdConnectConstants.Errors.InvalidRequest,
                    ErrorDescription = "A malformed token request has been received: make sure to use POST."
                }));
            }

            // See http://openid.net/specs/openid-connect-core-1_0.html#FormSerialization
            if (string.IsNullOrEmpty(Request.ContentType))
            {
                Logger.LogError("The token request was rejected because the " +
                                "mandatory 'Content-Type' header was missing.");

                return(await SendTokenResponseAsync(new OpenIdConnectResponse
                {
                    Error = OpenIdConnectConstants.Errors.InvalidRequest,
                    ErrorDescription = "A malformed token request has been received: " +
                                       "the mandatory 'Content-Type' header was missing from the POST request."
                }));
            }

            // May have media/type; charset=utf-8, allow partial match.
            if (!Request.ContentType.StartsWith("application/x-www-form-urlencoded", StringComparison.OrdinalIgnoreCase))
            {
                Logger.LogError("The token request was rejected because an invalid 'Content-Type' " +
                                "header was received: {ContentType}.", Request.ContentType);

                return(await SendTokenResponseAsync(new OpenIdConnectResponse
                {
                    Error = OpenIdConnectConstants.Errors.InvalidRequest,
                    ErrorDescription = "A malformed token request has been received: " +
                                       "the 'Content-Type' header contained an unexcepted value. " +
                                       "Make sure to use 'application/x-www-form-urlencoded'."
                }));
            }

            var request = new OpenIdConnectRequest(await Request.ReadFormAsync(Context.RequestAborted));

            // Note: set the message type before invoking the ExtractTokenRequest event.
            request.SetProperty(OpenIdConnectConstants.Properties.MessageType,
                                OpenIdConnectConstants.MessageTypes.TokenRequest);

            // Store the token request in the ASP.NET context.
            Context.SetOpenIdConnectRequest(request);

            var @event = new ExtractTokenRequestContext(Context, Options, request);
            await Options.Provider.ExtractTokenRequest(@event);

            if (@event.HandledResponse)
            {
                Logger.LogDebug("The token request was handled in user code.");

                return(true);
            }

            else if (@event.Skipped)
            {
                Logger.LogDebug("The default token request handling was skipped from user code.");

                return(false);
            }

            else if (@event.IsRejected)
            {
                Logger.LogError("The token request was rejected with the following error: {Error} ; {Description}",
                                /* Error: */ @event.Error ?? OpenIdConnectConstants.Errors.InvalidRequest,
                                /* Description: */ @event.ErrorDescription);

                return(await SendTokenResponseAsync(new OpenIdConnectResponse
                {
                    Error = @event.Error ?? OpenIdConnectConstants.Errors.InvalidRequest,
                    ErrorDescription = @event.ErrorDescription,
                    ErrorUri = @event.ErrorUri
                }));
            }

            Logger.LogInformation("The token request was successfully extracted " +
                                  "from the HTTP request: {Request}", request);

            // Reject token requests missing the mandatory grant_type parameter.
            if (string.IsNullOrEmpty(request.GrantType))
            {
                Logger.LogError("The token request was rejected because the grant type was missing.");

                return(await SendTokenResponseAsync(new OpenIdConnectResponse
                {
                    Error = OpenIdConnectConstants.Errors.InvalidRequest,
                    ErrorDescription = "The mandatory 'grant_type' parameter was missing.",
                }));
            }

            // Reject grant_type=authorization_code requests if the authorization endpoint is disabled.
            else if (request.IsAuthorizationCodeGrantType() && !Options.AuthorizationEndpointPath.HasValue)
            {
                Logger.LogError("The token request was rejected because the authorization code grant was disabled.");

                return(await SendTokenResponseAsync(new OpenIdConnectResponse
                {
                    Error = OpenIdConnectConstants.Errors.UnsupportedGrantType,
                    ErrorDescription = "The authorization code grant is not allowed by this authorization server."
                }));
            }

            // Reject grant_type=authorization_code requests missing the authorization code.
            // See https://tools.ietf.org/html/rfc6749#section-4.1.3
            else if (request.IsAuthorizationCodeGrantType() && string.IsNullOrEmpty(request.Code))
            {
                Logger.LogError("The token request was rejected because the authorization code was missing.");

                return(await SendTokenResponseAsync(new OpenIdConnectResponse
                {
                    Error = OpenIdConnectConstants.Errors.InvalidRequest,
                    ErrorDescription = "The mandatory 'code' parameter was missing."
                }));
            }

            // Reject grant_type=refresh_token requests missing the refresh token.
            // See https://tools.ietf.org/html/rfc6749#section-6
            else if (request.IsRefreshTokenGrantType() && string.IsNullOrEmpty(request.RefreshToken))
            {
                Logger.LogError("The token request was rejected because the refresh token was missing.");

                return(await SendTokenResponseAsync(new OpenIdConnectResponse
                {
                    Error = OpenIdConnectConstants.Errors.InvalidRequest,
                    ErrorDescription = "The mandatory 'refresh_token' parameter was missing."
                }));
            }

            // Reject grant_type=password requests missing username or password.
            // See https://tools.ietf.org/html/rfc6749#section-4.3.2
            else if (request.IsPasswordGrantType() && (string.IsNullOrEmpty(request.Username) ||
                                                       string.IsNullOrEmpty(request.Password)))
            {
                Logger.LogError("The token request was rejected because the resource owner credentials were missing.");

                return(await SendTokenResponseAsync(new OpenIdConnectResponse
                {
                    Error = OpenIdConnectConstants.Errors.InvalidRequest,
                    ErrorDescription = "The mandatory 'username' and/or 'password' parameters " +
                                       "was/were missing from the request message."
                }));
            }

            // When client_id and client_secret are both null, try to extract them from the Authorization header.
            // See http://tools.ietf.org/html/rfc6749#section-2.3.1 and
            // http://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication
            if (string.IsNullOrEmpty(request.ClientId) && string.IsNullOrEmpty(request.ClientSecret))
            {
                string header = Request.Headers[HeaderNames.Authorization];
                if (!string.IsNullOrEmpty(header) && header.StartsWith("Basic ", StringComparison.OrdinalIgnoreCase))
                {
                    try
                    {
                        var value = header.Substring("Basic ".Length).Trim();
                        var data  = Encoding.UTF8.GetString(Convert.FromBase64String(value));

                        var index = data.IndexOf(':');
                        if (index >= 0)
                        {
                            request.ClientId     = data.Substring(0, index);
                            request.ClientSecret = data.Substring(index + 1);
                        }
                    }

                    catch (FormatException) { }
                    catch (ArgumentException) { }
                }
            }

            var context = new ValidateTokenRequestContext(Context, Options, request);
            await Options.Provider.ValidateTokenRequest(context);

            // If the validation context was set as fully validated,
            // mark the OpenID Connect request as confidential.
            if (context.IsValidated)
            {
                request.SetProperty(OpenIdConnectConstants.Properties.ConfidentialityLevel,
                                    OpenIdConnectConstants.ConfidentialityLevels.Private);
            }

            if (context.HandledResponse)
            {
                Logger.LogDebug("The token request was handled in user code.");

                return(true);
            }

            else if (context.Skipped)
            {
                Logger.LogDebug("The default token request handling was skipped from user code.");

                return(false);
            }

            else if (context.IsRejected)
            {
                Logger.LogError("The token request was rejected with the following error: {Error} ; {Description}",
                                /* Error: */ context.Error ?? OpenIdConnectConstants.Errors.InvalidRequest,
                                /* Description: */ context.ErrorDescription);

                return(await SendTokenResponseAsync(new OpenIdConnectResponse
                {
                    Error = context.Error ?? OpenIdConnectConstants.Errors.InvalidRequest,
                    ErrorDescription = context.ErrorDescription,
                    ErrorUri = context.ErrorUri
                }));
            }

            // Reject grant_type=client_credentials requests if validation was skipped.
            else if (context.IsSkipped && request.IsClientCredentialsGrantType())
            {
                Logger.LogError("The token request must be fully validated to use the client_credentials grant type.");

                return(await SendTokenResponseAsync(new OpenIdConnectResponse
                {
                    Error = OpenIdConnectConstants.Errors.InvalidGrant,
                    ErrorDescription = "Client authentication is required when using client_credentials."
                }));
            }

            // At this stage, client_id cannot be null for grant_type=authorization_code requests,
            // as it must either be set in the ValidateTokenRequest notification
            // by the developer or manually flowed by non-confidential client applications.
            // See https://tools.ietf.org/html/rfc6749#section-4.1.3
            if (request.IsAuthorizationCodeGrantType() && string.IsNullOrEmpty(context.ClientId))
            {
                Logger.LogError("The token request was rejected because the mandatory 'client_id' was missing.");

                return(await SendTokenResponseAsync(new OpenIdConnectResponse
                {
                    Error = OpenIdConnectConstants.Errors.InvalidRequest,
                    ErrorDescription = "client_id was missing from the token request"
                }));
            }

            // Store the validated client_id as a request property.
            request.SetProperty(OpenIdConnectConstants.Properties.ClientId, context.ClientId);

            Logger.LogInformation("The token request was successfully validated.");

            AuthenticationTicket ticket = null;

            // See http://tools.ietf.org/html/rfc6749#section-4.1
            // and http://tools.ietf.org/html/rfc6749#section-4.1.3 (authorization code grant).
            // See http://tools.ietf.org/html/rfc6749#section-6 (refresh token grant).
            if (request.IsAuthorizationCodeGrantType() || request.IsRefreshTokenGrantType())
            {
                ticket = request.IsAuthorizationCodeGrantType() ?
                         await DeserializeAuthorizationCodeAsync(request.Code, request) :
                         await DeserializeRefreshTokenAsync(request.RefreshToken, request);

                if (ticket == null)
                {
                    Logger.LogError("The token request was rejected because the " +
                                    "authorization code or the refresh token was invalid.");

                    return(await SendTokenResponseAsync(new OpenIdConnectResponse
                    {
                        Error = OpenIdConnectConstants.Errors.InvalidGrant,
                        ErrorDescription = "Invalid ticket"
                    }));
                }

                // If the client was fully authenticated when retrieving its refresh token,
                // the current request must be rejected if client authentication was not enforced.
                if (request.IsRefreshTokenGrantType() && !context.IsValidated && ticket.IsConfidential())
                {
                    Logger.LogError("The token request was rejected because client authentication " +
                                    "was required to use the confidential refresh token.");

                    return(await SendTokenResponseAsync(new OpenIdConnectResponse
                    {
                        Error = OpenIdConnectConstants.Errors.InvalidGrant,
                        ErrorDescription = "Client authentication is required to use this ticket"
                    }));
                }

                if (ticket.Properties.ExpiresUtc.HasValue &&
                    ticket.Properties.ExpiresUtc < Options.SystemClock.UtcNow)
                {
                    Logger.LogError("The token request was rejected because the " +
                                    "authorization code or the refresh token was expired.");

                    return(await SendTokenResponseAsync(new OpenIdConnectResponse
                    {
                        Error = OpenIdConnectConstants.Errors.InvalidGrant,
                        ErrorDescription = "Expired ticket"
                    }));
                }

                // Note: presenters may be empty during a grant_type=refresh_token request if the refresh token
                // was issued to a public client but cannot be null for an authorization code grant request.
                var presenters = ticket.GetPresenters();
                if (request.IsAuthorizationCodeGrantType() && !presenters.Any())
                {
                    throw new InvalidOperationException("The presenters list cannot be extracted from the authorization code.");
                }

                // Ensure the authorization code/refresh token was issued to the client application making the token request.
                // Note: when using the refresh token grant, client_id is optional but must validated if present.
                // As a consequence, this check doesn't depend on the actual status of client authentication.
                // See https://tools.ietf.org/html/rfc6749#section-6
                // and http://openid.net/specs/openid-connect-core-1_0.html#RefreshingAccessToken
                if (!string.IsNullOrEmpty(context.ClientId) && presenters.Any() &&
                    !presenters.Contains(context.ClientId, StringComparer.Ordinal))
                {
                    Logger.LogError("The token request was rejected because the authorization " +
                                    "code was issued to a different client application.");

                    return(await SendTokenResponseAsync(new OpenIdConnectResponse
                    {
                        Error = OpenIdConnectConstants.Errors.InvalidGrant,
                        ErrorDescription = "Ticket does not contain matching client_id"
                    }));
                }

                // Validate the redirect_uri flowed by the client application during this token request.
                // Note: for pure OAuth2 requests, redirect_uri is only mandatory if the authorization request
                // contained an explicit redirect_uri. OpenID Connect requests MUST include a redirect_uri
                // but the specifications allow proceeding the token request without returning an error
                // if the authorization request didn't contain an explicit redirect_uri.
                // See https://tools.ietf.org/html/rfc6749#section-4.1.3
                // and http://openid.net/specs/openid-connect-core-1_0.html#TokenRequestValidation
                var address = ticket.GetProperty(OpenIdConnectConstants.Properties.RedirectUri);
                if (request.IsAuthorizationCodeGrantType() && !string.IsNullOrEmpty(address))
                {
                    if (string.IsNullOrEmpty(request.RedirectUri))
                    {
                        Logger.LogError("The token request was rejected because the mandatory 'redirect_uri' " +
                                        "parameter was missing from the grant_type=authorization_code request.");

                        return(await SendTokenResponseAsync(new OpenIdConnectResponse
                        {
                            Error = OpenIdConnectConstants.Errors.InvalidRequest,
                            ErrorDescription = "redirect_uri was missing from the token request"
                        }));
                    }

                    else if (!string.Equals(address, request.RedirectUri, StringComparison.Ordinal))
                    {
                        Logger.LogError("The token request was rejected because the 'redirect_uri' " +
                                        "parameter didn't correspond to the expected value.");

                        return(await SendTokenResponseAsync(new OpenIdConnectResponse
                        {
                            Error = OpenIdConnectConstants.Errors.InvalidGrant,
                            ErrorDescription = "Authorization code does not contain matching redirect_uri"
                        }));
                    }
                }

                // If a code challenge was initially sent in the authorization request and associated with the
                // code, validate the code verifier to ensure the token request is sent by a legit caller.
                var challenge = ticket.GetProperty(OpenIdConnectConstants.Properties.CodeChallenge);
                if (request.IsAuthorizationCodeGrantType() && !string.IsNullOrEmpty(challenge))
                {
                    // Get the code verifier from the token request.
                    // If it cannot be found, return an invalid_grant error.
                    var verifier = request.CodeVerifier;
                    if (string.IsNullOrEmpty(verifier))
                    {
                        Logger.LogError("The token request was rejected because the required 'code_verifier' " +
                                        "parameter was missing from the grant_type=authorization_code request.");

                        return(await SendTokenResponseAsync(new OpenIdConnectResponse
                        {
                            Error = OpenIdConnectConstants.Errors.InvalidRequest,
                            ErrorDescription = "The required 'code_verifier' was missing from the token request."
                        }));
                    }

                    // Note: the code challenge method is always validated when receiving the authorization request.
                    var method = ticket.GetProperty(OpenIdConnectConstants.Properties.CodeChallengeMethod);

                    Debug.Assert(string.IsNullOrEmpty(method) ||
                                 string.Equals(method, OpenIdConnectConstants.CodeChallengeMethods.Plain, StringComparison.Ordinal) ||
                                 string.Equals(method, OpenIdConnectConstants.CodeChallengeMethods.Sha256, StringComparison.Ordinal),
                                 "The specified code challenge method should be supported.");

                    // If the S256 challenge method was used, compute the hash corresponding to the code verifier.
                    if (string.Equals(method, OpenIdConnectConstants.CodeChallengeMethods.Sha256, StringComparison.Ordinal))
                    {
                        using (var algorithm = SHA256.Create())
                        {
                            // Compute the SHA-256 hash of the code verifier and encode it using base64-url.
                            // See https://tools.ietf.org/html/rfc7636#section-4.6 for more information.
                            var hash = algorithm.ComputeHash(Encoding.ASCII.GetBytes(request.CodeVerifier));

                            verifier = Base64UrlEncoder.Encode(hash);
                        }
                    }

                    // Compare the verifier and the code challenge: if the two don't match, return an error.
                    // Note: to prevent timing attacks, a time-constant comparer is always used.
                    if (!OpenIdConnectServerHelpers.AreEqual(verifier, challenge))
                    {
                        Logger.LogError("The token request was rejected because the 'code_verifier' was invalid.");

                        return(await SendTokenResponseAsync(new OpenIdConnectResponse
                        {
                            Error = OpenIdConnectConstants.Errors.InvalidGrant,
                            ErrorDescription = "The specified 'code_verifier' was invalid."
                        }));
                    }
                }

                if (request.IsRefreshTokenGrantType() && !string.IsNullOrEmpty(request.Resource))
                {
                    // When an explicit resource parameter has been included in the token request
                    // but was missing from the initial request, the request MUST be rejected.
                    var resources = ticket.GetResources();
                    if (!resources.Any())
                    {
                        Logger.LogError("The token request was rejected because the 'resource' parameter was not allowed.");

                        return(await SendTokenResponseAsync(new OpenIdConnectResponse
                        {
                            Error = OpenIdConnectConstants.Errors.InvalidGrant,
                            ErrorDescription = "Token request cannot contain a resource parameter " +
                                               "if the authorization request didn't contain one"
                        }));
                    }

                    // When an explicit resource parameter has been included in the token request,
                    // the authorization server MUST ensure that it doesn't contain resources
                    // that were not allowed during the initial authorization/token request.
                    else if (!new HashSet <string>(resources).IsSupersetOf(request.GetResources()))
                    {
                        Logger.LogError("The token request was rejected because the 'resource' parameter was not valid.");

                        return(await SendTokenResponseAsync(new OpenIdConnectResponse
                        {
                            Error = OpenIdConnectConstants.Errors.InvalidGrant,
                            ErrorDescription = "Token request doesn't contain a valid resource parameter"
                        }));
                    }
                }

                if (request.IsRefreshTokenGrantType() && !string.IsNullOrEmpty(request.Scope))
                {
                    // When an explicit scope parameter has been included in the token request
                    // but was missing from the initial request, the request MUST be rejected.
                    // See http://tools.ietf.org/html/rfc6749#section-6
                    var scopes = ticket.GetScopes();
                    if (!scopes.Any())
                    {
                        Logger.LogError("The token request was rejected because the 'scope' parameter was not allowed.");

                        return(await SendTokenResponseAsync(new OpenIdConnectResponse
                        {
                            Error = OpenIdConnectConstants.Errors.InvalidGrant,
                            ErrorDescription = "Token request cannot contain a scope parameter " +
                                               "if the authorization request didn't contain one"
                        }));
                    }

                    // When an explicit scope parameter has been included in the token request,
                    // the authorization server MUST ensure that it doesn't contain scopes
                    // that were not allowed during the initial authorization/token request.
                    // See https://tools.ietf.org/html/rfc6749#section-6
                    else if (!new HashSet <string>(scopes).IsSupersetOf(request.GetScopes()))
                    {
                        Logger.LogError("The token request was rejected because the 'scope' parameter was not valid.");

                        return(await SendTokenResponseAsync(new OpenIdConnectResponse
                        {
                            Error = OpenIdConnectConstants.Errors.InvalidGrant,
                            ErrorDescription = "Token request doesn't contain a valid scope parameter"
                        }));
                    }
                }
            }

            var notification = new HandleTokenRequestContext(Context, Options, request, ticket);
            await Options.Provider.HandleTokenRequest(notification);

            if (notification.HandledResponse)
            {
                Logger.LogDebug("The token request was handled in user code.");

                return(true);
            }

            else if (notification.Skipped)
            {
                Logger.LogDebug("The default token request handling was skipped from user code.");

                return(false);
            }

            else if (notification.IsRejected)
            {
                Logger.LogError("The token request was rejected with the following error: {Error} ; {Description}",
                                /* Error: */ notification.Error ?? OpenIdConnectConstants.Errors.InvalidGrant,
                                /* Description: */ notification.ErrorDescription);

                return(await SendTokenResponseAsync(new OpenIdConnectResponse
                {
                    Error = notification.Error ?? OpenIdConnectConstants.Errors.InvalidGrant,
                    ErrorDescription = notification.ErrorDescription,
                    ErrorUri = notification.ErrorUri
                }));
            }

            // Flow the changes made to the ticket.
            ticket = notification.Ticket;

            // Ensure an authentication ticket has been provided or return
            // an error code indicating that the grant type is not supported.
            if (ticket == null)
            {
                Logger.LogError("The token request was rejected because no authentication " +
                                "ticket was returned by application code.");

                return(await SendTokenResponseAsync(new OpenIdConnectResponse
                {
                    Error = OpenIdConnectConstants.Errors.UnsupportedGrantType,
                    ErrorDescription = "The specified grant_type parameter is not supported."
                }));
            }

            // Call HandleSignInAsync to generate a token response.
            return(await HandleSignInAsync(ticket));
        }
예제 #3
0
 /// <summary>
 /// Called at the final stage of a successful Token endpoint request.
 /// An application may implement this call in order to do any final
 /// modification of the claims being used to issue access or refresh tokens.
 /// </summary>
 /// <param name="context">The context of the event carries information in and results out.</param>
 /// <returns>Task to enable asynchronous execution</returns>
 public virtual Task HandleTokenRequest(HandleTokenRequestContext context) => OnHandleTokenRequest(context);