protected override async Task <bool> HandleForbiddenAsync(ChallengeContext context)
        {
            // Stop processing the request if no OpenID Connect
            // message has been found in the current context.
            var request = Context.GetOpenIdConnectRequest();

            if (request == null)
            {
                return(false);
            }

            var response = new OpenIdConnectMessage {
                Error            = OpenIdConnectConstants.Errors.AccessDenied,
                ErrorDescription = "The authorization grant has been denied by the resource owner",
                RedirectUri      = request.RedirectUri,
                State            = request.State
            };

            // Create a new ticket containing an empty identity and
            // the authentication properties extracted from the challenge.
            var ticket = new AuthenticationTicket(
                new ClaimsPrincipal(),
                new AuthenticationProperties(context.Properties),
                context.AuthenticationScheme);

            var notification = new ApplyAuthorizationResponseContext(Context, Options, ticket, request, response);
            await Options.Provider.ApplyAuthorizationResponse(notification);

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

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

            return(await SendErrorRedirectAsync(request, response));
        }
        private async Task <bool> SendAuthorizationResponseAsync(OpenIdConnectResponse response, AuthenticationTicket ticket = null)
        {
            var request = Context.GetOpenIdConnectRequest();

            Context.SetOpenIdConnectResponse(response);

            response.SetProperty(OpenIdConnectConstants.Properties.MessageType,
                                 OpenIdConnectConstants.MessageTypes.AuthorizationResponse);

            // Note: as this stage, the request may be null (e.g if it couldn't be extracted from the HTTP request).
            var notification = new ApplyAuthorizationResponseContext(Context, Scheme, Options, ticket, request, response)
            {
                RedirectUri  = request?.GetProperty <string>(OpenIdConnectConstants.Properties.ValidatedRedirectUri),
                ResponseMode = request?.ResponseMode
            };

            // If the response_mode parameter was not specified, try to infer it.
            if (string.IsNullOrEmpty(notification.ResponseMode) && !string.IsNullOrEmpty(notification.RedirectUri))
            {
                notification.ResponseMode =
                    request.IsFormPostResponseMode() ? OpenIdConnectConstants.ResponseModes.FormPost :
                    request.IsFragmentResponseMode() ? OpenIdConnectConstants.ResponseModes.Fragment :
                    request.IsQueryResponseMode()    ? OpenIdConnectConstants.ResponseModes.Query    : null;
            }

            await Provider.ApplyAuthorizationResponse(notification);


            if (notification.Result != null)
            {
                if (notification.Result.Handled)
                {
                    Logger.LogDebug("The authorization request was handled in user code.");

                    return(true);
                }

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

                    return(false);
                }
            }

            // Directly display an error page if redirect_uri cannot be used to
            // redirect the user agent back to the client application.
            if (!string.IsNullOrEmpty(response.Error) && string.IsNullOrEmpty(notification.RedirectUri))
            {
                // Apply a 400 status code by default.
                Response.StatusCode = 400;

                if (Options.ApplicationCanDisplayErrors)
                {
                    // Return false to allow the rest of
                    // the pipeline to handle the request.
                    return(false);
                }

                Logger.LogInformation("The authorization response was successfully returned " +
                                      "as a plain-text document: {Response}.", response);

                return(await SendNativePageAsync(response));
            }

            // At this stage, throw an exception if the request was not properly extracted.
            if (request == null)
            {
                throw new InvalidOperationException("The authorization response cannot be returned.");
            }

            // Attach the request state to the authorization response.
            if (string.IsNullOrEmpty(response.State))
            {
                response.State = request.State;
            }

            // Create a new parameters dictionary holding the name/value pairs.
            var parameters = new Dictionary <string, string>();

            foreach (var parameter in response.GetParameters())
            {
                // Ignore null or empty parameters, including JSON
                // objects that can't be represented as strings.
                var value = (string)parameter.Value;
                if (string.IsNullOrEmpty(value))
                {
                    continue;
                }

                parameters.Add(parameter.Key, value);
            }

            // Note: at this stage, the redirect_uri parameter MUST be trusted.
            switch (notification.ResponseMode)
            {
            case OpenIdConnectConstants.ResponseModes.FormPost:
            {
                Logger.LogInformation("The authorization response was successfully returned to " +
                                      "'{RedirectUri}' using the form post response mode: {Response}.",
                                      notification.RedirectUri, response);

                using (var buffer = new MemoryStream())
                    using (var writer = new StreamWriter(buffer))
                    {
                        writer.WriteLine("<!doctype html>");
                        writer.WriteLine("<html>");
                        writer.WriteLine("<body>");

                        // While the redirect_uri parameter should be guarded against unknown values
                        // by OpenIdConnectServerProvider.ValidateAuthorizationRequest,
                        // it's still safer to encode it to avoid cross-site scripting attacks
                        // if the authorization server has a relaxed policy concerning redirect URIs.
                        writer.WriteLine($@"<form name=""form"" method=""post"" action=""{Options.HtmlEncoder.Encode(notification.RedirectUri)}"">");

                        foreach (var parameter in parameters)
                        {
                            var key   = Options.HtmlEncoder.Encode(parameter.Key);
                            var value = Options.HtmlEncoder.Encode(parameter.Value);

                            writer.WriteLine($@"<input type=""hidden"" name=""{key}"" value=""{value}"" />");
                        }

                        writer.WriteLine(@"<noscript>Click here to finish the authorization process: <input type=""submit"" /></noscript>");
                        writer.WriteLine("</form>");
                        writer.WriteLine("<script>document.form.submit();</script>");
                        writer.WriteLine("</body>");
                        writer.WriteLine("</html>");
                        writer.Flush();

                        Response.StatusCode    = 200;
                        Response.ContentLength = buffer.Length;
                        Response.ContentType   = "text/html;charset=UTF-8";

                        Response.Headers["Cache-Control"] = "no-cache";
                        Response.Headers["Pragma"]        = "no-cache";
                        Response.Headers["Expires"]       = "-1";

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

                        return(true);
                    }
            }

            case OpenIdConnectConstants.ResponseModes.Fragment:
            {
                Logger.LogInformation("The authorization response was successfully returned to " +
                                      "'{RedirectUri}' using the fragment response mode: {Response}.",
                                      notification.RedirectUri, response);

                var location = notification.RedirectUri;
                var appender = new OpenIdConnectServerHelpers.Appender(location, '#');

                foreach (var parameter in parameters)
                {
                    appender.Append(parameter.Key, parameter.Value);
                }

                Response.Redirect(appender.ToString());
                return(true);
            }

            case OpenIdConnectConstants.ResponseModes.Query:
            {
                Logger.LogInformation("The authorization response was successfully returned to " +
                                      "'{RedirectUri}' using the query response mode: {Response}.",
                                      notification.RedirectUri, response);

                var location = QueryHelpers.AddQueryString(notification.RedirectUri, parameters);

                Response.Redirect(location);
                return(true);
            }

            default:
            {
                Logger.LogError("The authorization request was rejected because the 'response_mode' " +
                                "parameter was invalid: {ResponseMode}.", request.ResponseMode);

                return(await SendNativePageAsync(new OpenIdConnectResponse
                    {
                        Error = OpenIdConnectConstants.Errors.InvalidRequest,
                        ErrorDescription = "The specified 'response_mode' parameter is not supported."
                    }));
            }
            }
        }
Exemple #3
0
 /// <summary>
 /// Called before the AuthorizationEndpoint redirects its response to the caller.
 /// The response could contain an access token when using implicit flow or
 /// an authorization code when using the authorization code flow.
 /// If the web application wishes to produce the authorization response directly in the AuthorizationEndpoint call it may write to the
 /// context.Response directly and should call context.RequestCompleted to stop other handlers from executing.
 /// This call may also be used to add additional response parameters to the authorization response.
 /// </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 ApplyAuthorizationResponse(ApplyAuthorizationResponseContext context) => OnApplyAuthorizationResponse(context);
        private async Task <bool> SendAuthorizationResponseAsync(OpenIdConnectResponse response, AuthenticationTicket ticket = null)
        {
            var request = Context.GetOpenIdConnectRequest();

            if (request == null)
            {
                request = new OpenIdConnectRequest();
            }

            Context.SetOpenIdConnectResponse(response);

            var notification = new ApplyAuthorizationResponseContext(Context, Options, ticket, request, response);
            await Options.Provider.ApplyAuthorizationResponse(notification);

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

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

            if (!string.IsNullOrEmpty(response.Error))
            {
                // Directly display an error page if redirect_uri cannot be used to
                // redirect the user agent back to the client application.
                if (string.IsNullOrEmpty(response.RedirectUri))
                {
                    // Apply a 400 status code by default.
                    Response.StatusCode = 400;

                    if (Options.ApplicationCanDisplayErrors)
                    {
                        // Return false to allow the rest of
                        // the pipeline to handle the request.
                        return(false);
                    }

                    return(await SendNativePageAsync(response));
                }
            }

            // Create a new parameters dictionary holding the name/value pairs.
            var parameters = new Dictionary <string, string>();

            foreach (var parameter in response.GetParameters())
            {
                // Don't include redirect_uri in the parameters dictionary.
                if (string.Equals(parameter.Key, OpenIdConnectConstants.Parameters.RedirectUri, StringComparison.Ordinal))
                {
                    continue;
                }

                var value = parameter.Value as JValue;
                if (value == null)
                {
                    Logger.LogWarning("A parameter whose type was incompatible was ignored and excluded " +
                                      "from the authorization response: '{Parameter}'.", parameter.Key);

                    continue;
                }

                parameters.Add(parameter.Key, (string)value);
            }

            // Note: at this stage, the redirect_uri parameter MUST be trusted.
            if (request.IsFormPostResponseMode())
            {
                using (var buffer = new MemoryStream())
                    using (var writer = new StreamWriter(buffer)) {
                        writer.WriteLine("<!doctype html>");
                        writer.WriteLine("<html>");
                        writer.WriteLine("<body>");

                        // While the redirect_uri parameter should be guarded against unknown values
                        // by OpenIdConnectServerProvider.ValidateAuthorizationRequest,
                        // it's still safer to encode it to avoid cross-site scripting attacks
                        // if the authorization server has a relaxed policy concerning redirect URIs.
                        writer.WriteLine($"<form name='form' method='post' action='{Options.HtmlEncoder.Encode(response.RedirectUri)}'>");

                        foreach (var parameter in parameters)
                        {
                            var key   = Options.HtmlEncoder.Encode(parameter.Key);
                            var value = Options.HtmlEncoder.Encode(parameter.Value);

                            writer.WriteLine($"<input type='hidden' name='{key}' value='{value}' />");
                        }

                        writer.WriteLine("<noscript>Click here to finish the authorization process: <input type='submit' /></noscript>");
                        writer.WriteLine("</form>");
                        writer.WriteLine("<script>document.form.submit();</script>");
                        writer.WriteLine("</body>");
                        writer.WriteLine("</html>");
                        writer.Flush();

                        Response.StatusCode    = 200;
                        Response.ContentLength = buffer.Length;
                        Response.ContentType   = "text/html;charset=UTF-8";

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

                        return(true);
                    }
            }

            else if (request.IsFragmentResponseMode())
            {
                var location = response.RedirectUri;
                var appender = new Appender(location, '#');

                foreach (var parameter in parameters)
                {
                    appender.Append(parameter.Key, parameter.Value);
                }

                Response.Redirect(appender.ToString());
                return(true);
            }

            else if (request.IsQueryResponseMode())
            {
                var location = QueryHelpers.AddQueryString(response.RedirectUri, parameters);

                Response.Redirect(location);
                return(true);
            }

            Logger.LogError("The authorization request was rejected because the 'response_mode' " +
                            "parameter was invalid: {ResponseMode}.", request.ResponseMode);

            return(await SendNativePageAsync(new OpenIdConnectResponse {
                Error = OpenIdConnectConstants.Errors.InvalidRequest,
                ErrorDescription = "response_mode unsupported"
            }));
        }
        private async Task <bool> SendAuthorizationResponseAsync(
            OpenIdConnectMessage request, OpenIdConnectMessage response, AuthenticationTicket ticket = null)
        {
            if (request == null)
            {
                request = new OpenIdConnectMessage();
            }

            var notification = new ApplyAuthorizationResponseContext(Context, Options, ticket, request, response);
            await Options.Provider.ApplyAuthorizationResponse(notification);

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

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

            if (!string.IsNullOrEmpty(response.Error))
            {
                // When returning an error, remove the authorization request from the ASP.NET context
                // to inform the application code that there's nothing more to handle.
                Context.SetOpenIdConnectRequest(request: null);

                // Directly display an error page if redirect_uri cannot be used to
                // redirect the user agent back to the client application.
                if (string.IsNullOrEmpty(response.RedirectUri))
                {
                    // Apply a 400 status code by default.
                    Response.StatusCode = 400;

                    if (Options.ApplicationCanDisplayErrors)
                    {
                        Context.SetOpenIdConnectResponse(response);

                        // Return false to allow the rest of
                        // the pipeline to handle the request.
                        return(false);
                    }

                    return(await SendNativePageAsync(response));
                }
            }

            // Note: at this stage, the redirect_uri parameter MUST be trusted.
            if (request.IsFormPostResponseMode())
            {
                using (var buffer = new MemoryStream())
                    using (var writer = new StreamWriter(buffer)) {
                        writer.WriteLine("<!doctype html>");
                        writer.WriteLine("<html>");
                        writer.WriteLine("<body>");

                        // While the redirect_uri parameter should be guarded against unknown values
                        // by IOpenIdConnectServerProvider.ValidateAuthorizationRequest,
                        // it's still safer to encode it to avoid cross-site scripting attacks
                        // if the authorization server has a relaxed policy concerning redirect URIs.
                        writer.WriteLine($"<form name='form' method='post' action='{Options.HtmlEncoder.Encode(response.RedirectUri)}'>");

                        foreach (var parameter in response.Parameters)
                        {
                            // Don't include redirect_uri in the form.
                            if (string.Equals(parameter.Key, OpenIdConnectParameterNames.RedirectUri, StringComparison.Ordinal))
                            {
                                continue;
                            }

                            var key   = Options.HtmlEncoder.Encode(parameter.Key);
                            var value = Options.HtmlEncoder.Encode(parameter.Value);

                            writer.WriteLine($"<input type='hidden' name='{key}' value='{value}' />");
                        }

                        writer.WriteLine("<noscript>Click here to finish the authorization process: <input type='submit' /></noscript>");
                        writer.WriteLine("</form>");
                        writer.WriteLine("<script>document.form.submit();</script>");
                        writer.WriteLine("</body>");
                        writer.WriteLine("</html>");
                        writer.Flush();

                        Response.StatusCode    = 200;
                        Response.ContentLength = buffer.Length;
                        Response.ContentType   = "text/html;charset=UTF-8";

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

                        return(true);
                    }
            }

            else if (request.IsFragmentResponseMode())
            {
                var location = response.RedirectUri;
                var appender = new Appender(location, '#');

                foreach (var parameter in response.Parameters)
                {
                    // Don't include redirect_uri in the fragment.
                    if (string.Equals(parameter.Key, OpenIdConnectParameterNames.RedirectUri, StringComparison.Ordinal))
                    {
                        continue;
                    }

                    appender.Append(parameter.Key, parameter.Value);
                }

                Response.Redirect(appender.ToString());
                return(true);
            }

            else if (request.IsQueryResponseMode())
            {
                var location = response.RedirectUri;

                foreach (var parameter in response.Parameters)
                {
                    // Don't include redirect_uri in the query string.
                    if (string.Equals(parameter.Key, OpenIdConnectParameterNames.RedirectUri, StringComparison.Ordinal))
                    {
                        continue;
                    }

                    location = QueryHelpers.AddQueryString(location, parameter.Key, parameter.Value);
                }

                Response.Redirect(location);
                return(true);
            }

            return(await SendNativePageAsync(response));
        }
        protected override async Task HandleSignInAsync(SignInContext context)
        {
            // request may be null when no authorization request has been received
            // or has been already handled by InvokeAuthorizationEndpointAsync.
            var request = Context.GetOpenIdConnectRequest();

            if (request == null)
            {
                return;
            }

            // Stop processing the request if there's no response grant that matches
            // the authentication type associated with this middleware instance
            // or if the response status code doesn't indicate a successful response.
            if (context == null || Response.StatusCode != 200)
            {
                return;
            }

            if (!context.Principal.HasClaim(claim => claim.Type == ClaimTypes.NameIdentifier))
            {
                throw new InvalidOperationException("The authentication ticket was rejected because it didn't " +
                                                    "contain the mandatory ClaimTypes.NameIdentifier claim.");
            }

            var response = new OpenIdConnectMessage {
                RedirectUri = request.RedirectUri,
                State       = request.State
            };

            if (!string.IsNullOrEmpty(request.Nonce))
            {
                // Keep the original nonce parameter for later comparison.
                context.Properties[OpenIdConnectConstants.Properties.Nonce] = request.Nonce;
            }

            if (!string.IsNullOrEmpty(request.RedirectUri))
            {
                // Keep the original redirect_uri parameter for later comparison.
                context.Properties[OpenIdConnectConstants.Properties.RedirectUri] = request.RedirectUri;
            }

            // Always include the "openid" scope when the developer doesn't explicitly call SetScopes.
            // Note: the application is allowed to specify a different "scopes"
            // parameter when calling AuthenticationManager.SignInAsync: in this case,
            // don't replace the "scopes" property stored in the authentication ticket.
            if (!context.Properties.ContainsKey(OpenIdConnectConstants.Properties.Scopes) &&
                request.HasScope(OpenIdConnectConstants.Scopes.OpenId))
            {
                context.Properties[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 (!context.Properties.ContainsKey(OpenIdConnectConstants.Properties.Resources) &&
                context.Properties.TryGetValue(OpenIdConnectConstants.Properties.Audiences, out audiences))
            {
                context.Properties[OpenIdConnectConstants.Properties.Resources] = audiences;
            }

            // Determine whether an authorization code should be returned
            // and invoke SerializeAuthorizationCodeAsync if necessary.
            if (request.HasResponseType(OpenIdConnectConstants.ResponseTypes.Code))
            {
                // Make sure to create a copy of the authentication properties
                // to avoid modifying the properties set on the original ticket.
                var properties = new AuthenticationProperties(context.Properties).Copy();

                // properties.IssuedUtc and properties.ExpiresUtc are always
                // explicitly set to null to avoid aligning the expiration date
                // of the authorization code with the lifetime of the other tokens.
                properties.IssuedUtc = properties.ExpiresUtc = null;

                response.Code = await SerializeAuthorizationCodeAsync(context.Principal, properties, request, response);

                // Ensure that an authorization code 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.Code))
                {
                    throw new InvalidOperationException("An error occurred during the serialization of the " +
                                                        "authorization code and a null value was returned.");
                }
            }

            // Determine whether an access token should be returned
            // and invoke SerializeAccessTokenAsync if necessary.
            if (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 = new AuthenticationProperties(context.Properties).Copy();

                // Note: when the "resource" parameter added to the OpenID Connect response
                // is identical to the request parameter, returning it is not necessary.
                var resources = properties.GetProperty(OpenIdConnectConstants.Properties.Resources);
                if (!string.IsNullOrEmpty(request.Resource) && !string.IsNullOrEmpty(resources) &&
                    !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, returning it is not necessary.
                var scopes = properties.GetProperty(OpenIdConnectConstants.Properties.Scopes);
                if (!string.IsNullOrEmpty(request.Scope) && !string.IsNullOrEmpty(scopes) &&
                    !string.Equals(request.Scope, scopes, StringComparison.Ordinal))
                {
                    response.Scope = scopes;
                }

                response.TokenType   = OpenIdConnectConstants.TokenTypes.Bearer;
                response.AccessToken = await SerializeAccessTokenAsync(context.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))
                {
                    throw new InvalidOperationException("An error occurred during the serialization of the " +
                                                        "access token and a null value was returned.");
                }

                // 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);
                }
            }

            // Determine whether an identity token should be returned
            // and invoke SerializeIdentityTokenAsync if necessary.
            // Note: the identity token MUST be created after the authorization code
            // and the access token to create appropriate at_hash and c_hash claims.
            if (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 = new AuthenticationProperties(context.Properties).Copy();

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

                // Ensure that an identity 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.IdToken))
                {
                    throw new InvalidOperationException("An error occurred during the serialization of the " +
                                                        "identity token and a null value was returned.");
                }
            }

            // Remove the OpenID Connect request from the distributed cache.
            var identifier = request.GetRequestId();

            if (!string.IsNullOrEmpty(identifier))
            {
                await Options.Cache.RemoveAsync($"asos-request:{identifier}");
            }

            var ticket = new AuthenticationTicket(context.Principal,
                                                  new AuthenticationProperties(context.Properties),
                                                  context.AuthenticationScheme);

            var notification = new ApplyAuthorizationResponseContext(Context, Options, ticket, request, response);
            await Options.Provider.ApplyAuthorizationResponse(notification);

            if (notification.HandledResponse)
            {
                return;
            }

            else if (notification.Skipped)
            {
                return;
            }

            await SendAuthorizationResponseAsync(request, response);
        }