protected virtual async Task <bool> VerifyAssertionAsync([NotNull] OpenIdAuthenticationMessage message) { // Create a new message to store the parameters sent to the identity provider. // Note: using a dictionary is safe as OpenID 2.0 parameters are supposed to be unique. // See http://openid.net/specs/openid-authentication-2_0.html#anchor4 var payload = new Dictionary <string, string> { [$"{OpenIdAuthenticationConstants.Prefixes.OpenId}." + OpenIdAuthenticationConstants.Parameters.Mode] = OpenIdAuthenticationConstants.Modes.CheckAuthentication }; // Copy the parameters extracted from the assertion. foreach (var parameter in message.Parameters) { if (string.Equals(parameter.Key, $"{OpenIdAuthenticationConstants.Prefixes.OpenId}." + OpenIdAuthenticationConstants.Parameters.Mode, StringComparison.Ordinal)) { continue; } // Note: the "state" parameter is ignored as it is not part of the // OpenID message but directly flowed in the return_to parameter. if (string.Equals(parameter.Key, OpenIdAuthenticationConstants.Parameters.State, StringComparison.Ordinal)) { continue; } payload.Add(parameter.Key, parameter.Value); } // Create a new check_authentication request to verify the assertion. var request = new HttpRequestMessage(HttpMethod.Post, Options.Endpoint); request.Content = new FormUrlEncodedContent(payload); var response = await Options.HttpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, Context.RequestAborted); if (!response.IsSuccessStatusCode) { Logger.LogWarning("The authentication failed because an invalid check_authentication response was received: " + "the identity provider returned a {Status} response with the following payload: {Headers} {Body}.", /* Status: */ response.StatusCode, /* Headers: */ response.Headers.ToString(), /* Body: */ await response.Content.ReadAsStringAsync()); return(false); } using (var stream = await response.Content.ReadAsStreamAsync()) using (var reader = new StreamReader(stream)) { // Create a new dictionary containing the parameters extracted from the response body. var parameters = new Dictionary <string, string>(StringComparer.Ordinal); // Note: the response is encoded using the 'Key-Value Form Encoding'. // See http://openid.net/specs/openid-authentication-2_0.html#anchor4 for (var line = await reader.ReadLineAsync(); line != null; line = await reader.ReadLineAsync()) { var parameter = line.Split(':'); if (parameter.Length != 2) { continue; } parameters.Add(parameter[0], parameter[1]); } // Stop processing the assertion if the mandatory is_valid // parameter was missing from the response body. if (!parameters.ContainsKey(OpenIdAuthenticationConstants.Parameters.IsValid)) { Logger.LogWarning("The authentication response was rejected because the identity provider " + "returned an invalid check_authentication response."); return(false); } // Stop processing the assertion if the authentication server declared it as invalid. if (!string.Equals(parameters[OpenIdAuthenticationConstants.Parameters.IsValid], "true", StringComparison.Ordinal)) { Logger.LogWarning("The authentication response was rejected because the identity provider " + "declared the security assertion as invalid."); return(false); } } return(true); }
protected virtual Task <string> GenerateChallengeUrlAsync([NotNull] OpenIdAuthenticationMessage message) { return(Task.FromResult(QueryHelpers.AddQueryString(Options.Endpoint.AbsoluteUri, message.Parameters))); }
protected override async Task <bool> HandleUnauthorizedAsync(ChallengeContext context) { var properties = new AuthenticationProperties(context.Properties); if (Options.Endpoint == null) { // Note: altering options during a request is not thread safe but // would only result in multiple discovery requests in the worst case. Options.Endpoint = await DiscoverEndpointAsync(Options.Authority); } if (Options.Endpoint == null) { Logger.LogError("The user agent cannot be redirected to the identity provider because no " + "endpoint was registered in the options or discovered through Yadis."); return(true); } // Determine the realm using the current address // if one has not been explicitly provided; var realm = Options.Realm; if (string.IsNullOrEmpty(realm)) { realm = Request.Scheme + "://" + Request.Host + OriginalPathBase; } // Use the current address as the final location where the user agent // will be redirected to if one has not been explicitly provided. if (string.IsNullOrEmpty(properties.RedirectUri)) { properties.RedirectUri = Request.Scheme + "://" + Request.Host + OriginalPathBase + Request.Path + Request.QueryString; } // Store the return_to parameter for later comparison. properties.Items[OpenIdAuthenticationConstants.Properties.ReturnTo] = Request.Scheme + "://" + Request.Host + OriginalPathBase + Options.CallbackPath; // Generate a new anti-forgery token. GenerateCorrelationId(properties); var state = UrlEncoder.Encode(Options.StateDataFormat.Protect(properties)); // Create a new message containing the OpenID 2.0 request parameters. // See http://openid.net/specs/openid-authentication-2_0.html#requesting_authentication var message = new OpenIdAuthenticationMessage { ClaimedIdentifier = "http://specs.openid.net/auth/2.0/identifier_select", Identity = "http://specs.openid.net/auth/2.0/identifier_select", Mode = OpenIdAuthenticationConstants.Modes.CheckIdSetup, Namespace = OpenIdAuthenticationConstants.Namespaces.OpenId, Realm = realm, ReturnTo = QueryHelpers.AddQueryString( uri: properties.Items[OpenIdAuthenticationConstants.Properties.ReturnTo], name: OpenIdAuthenticationConstants.Parameters.State, value: state) }; if (Options.Attributes.Count != 0) { // openid.ns.ax (http://openid.net/srv/ax/1.0) message.SetParameter( prefix: OpenIdAuthenticationConstants.Prefixes.Namespace, name: OpenIdAuthenticationConstants.Aliases.Ax, value: OpenIdAuthenticationConstants.Namespaces.Ax); // openid.ax.mode (fetch_request) message.SetParameter( prefix: OpenIdAuthenticationConstants.Prefixes.Ax, name: OpenIdAuthenticationConstants.Parameters.Mode, value: OpenIdAuthenticationConstants.Modes.FetchRequest); foreach (var attribute in Options.Attributes) { message.SetParameter( prefix: OpenIdAuthenticationConstants.Prefixes.Ax, name: $"{OpenIdAuthenticationConstants.Prefixes.Type}.{attribute.Key}", value: attribute.Value); } // openid.ax.required message.SetParameter( prefix: OpenIdAuthenticationConstants.Prefixes.Ax, name: OpenIdAuthenticationConstants.Parameters.Required, value: string.Join(",", Options.Attributes.Select(attribute => attribute.Key))); } Response.Redirect(await GenerateChallengeUrlAsync(message)); return(true); }
protected override async Task <AuthenticateResult> HandleRemoteAuthenticateAsync() { // Always extract the "state" parameter from the query string. var state = Request.Query[OpenIdAuthenticationConstants.Parameters.State]; if (string.IsNullOrEmpty(state)) { return(AuthenticateResult.Fail("The authentication response was rejected " + "because the state parameter was missing.")); } var properties = Options.StateDataFormat.Unprotect(state); if (properties == null) { return(AuthenticateResult.Fail("The authentication response was rejected " + "because the state parameter was invalid.")); } // Validate the anti-forgery token. if (!ValidateCorrelationId(properties)) { return(AuthenticateResult.Fail("The authentication response was rejected " + "because the anti-forgery token was invalid.")); } OpenIdAuthenticationMessage message; // OpenID 2.0 responses MUST necessarily be made using either GET or POST. // See http://openid.net/specs/openid-authentication-2_0.html#anchor4 if (!string.Equals(Request.Method, "GET", StringComparison.OrdinalIgnoreCase) && !string.Equals(Request.Method, "POST", StringComparison.OrdinalIgnoreCase)) { return(AuthenticateResult.Fail("The authentication response was rejected because it was made " + "using an invalid method: make sure to use either GET or POST.")); } if (string.Equals(Request.Method, "GET", StringComparison.OrdinalIgnoreCase)) { message = new OpenIdAuthenticationMessage(Request.Query); } else { // OpenID 2.0 responses MUST include a Content-Type header when using POST. // See http://openid.net/specs/openid-authentication-2_0.html#anchor4 if (string.IsNullOrEmpty(Request.ContentType)) { return(AuthenticateResult.Fail("The authentication response was rejected because " + "it was missing the mandatory 'Content-Type' header.")); } // May have media/type; charset=utf-8, allow partial match. if (!Request.ContentType.StartsWith("application/x-www-form-urlencoded", StringComparison.OrdinalIgnoreCase)) { return(AuthenticateResult.Fail("The authentication response was rejected because an invalid Content-Type header " + "was received: make sure to use 'application/x-www-form-urlencoded'.")); } message = new OpenIdAuthenticationMessage(await Request.ReadFormAsync(Context.RequestAborted)); } // Ensure that the current request corresponds to an OpenID 2.0 assertion. if (!string.Equals(message.Namespace, OpenIdAuthenticationConstants.Namespaces.OpenId, StringComparison.Ordinal)) { return(AuthenticateResult.Fail("The authentication response was rejected because it was missing the mandatory " + "'openid.ns' parameter or because an unsupported version of OpenID was used.")); } // Stop processing the message if the authentication process was cancelled by the user. if (string.Equals(message.Mode, OpenIdAuthenticationConstants.Modes.Cancel, StringComparison.Ordinal)) { return(AuthenticateResult.Fail("The authentication response was rejected because " + "the operation was cancelled by the user.")); } // Stop processing the message if an error was returned by the provider. else if (string.Equals(message.Mode, OpenIdAuthenticationConstants.Modes.Error, StringComparison.Ordinal)) { if (string.IsNullOrEmpty(message.Error)) { return(AuthenticateResult.Fail("The authentication response was rejected because an " + "unspecified error was returned by the identity provider.")); } return(AuthenticateResult.Fail("The authentication response was rejected because " + $"an error was returned by the identity provider: {message.Error}.")); } // At this point, stop processing the message if the assertion was not positive. else if (!string.Equals(message.Mode, OpenIdAuthenticationConstants.Modes.IdRes, StringComparison.Ordinal)) { return(AuthenticateResult.Fail("The authentication response was rejected because " + "the identity provider declared it as invalid.")); } // Stop processing the message if the assertion // was not validated by the identity provider. if (!await VerifyAssertionAsync(message)) { return(AuthenticateResult.Fail("The authentication response was rejected by the identity provider.")); } var address = QueryHelpers.AddQueryString(uri: properties.Items[OpenIdAuthenticationConstants.Properties.ReturnTo], name: OpenIdAuthenticationConstants.Parameters.State, value: state); // Validate the return_to parameter by comparing it to the address stored in the properties. // See http://openid.net/specs/openid-authentication-2_0.html#verify_return_to if (!string.Equals(message.ReturnTo, address, StringComparison.Ordinal)) { return(AuthenticateResult.Fail("The authentication response was rejected because the return_to parameter was invalid.")); } // Make sure the OpenID 2.0 assertion contains an identifier. if (string.IsNullOrEmpty(message.ClaimedIdentifier)) { return(AuthenticateResult.Fail("The authentication response was rejected because it " + "was missing the mandatory 'claimed_id' parameter.")); } var identity = new ClaimsIdentity(Options.AuthenticationScheme); // Add the claimed identifier to the identity. identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, message.ClaimedIdentifier, ClaimValueTypes.String, Options.ClaimsIssuer)); // Add the most common attributes to the identity. var attributes = message.GetAttributes(); foreach (var attribute in attributes) { // http://axschema.org/contact/email if (string.Equals(attribute.Key, OpenIdAuthenticationConstants.Attributes.Email, StringComparison.Ordinal)) { identity.AddClaim(new Claim(ClaimTypes.Email, attribute.Value, ClaimValueTypes.Email, Options.ClaimsIssuer)); } // http://axschema.org/namePerson else if (string.Equals(attribute.Key, OpenIdAuthenticationConstants.Attributes.Name, StringComparison.Ordinal)) { identity.AddClaim(new Claim(ClaimTypes.Name, attribute.Value, ClaimValueTypes.String, Options.ClaimsIssuer)); } // http://axschema.org/namePerson/first else if (string.Equals(attribute.Key, OpenIdAuthenticationConstants.Attributes.Firstname, StringComparison.Ordinal)) { identity.AddClaim(new Claim(ClaimTypes.GivenName, attribute.Value, ClaimValueTypes.String, Options.ClaimsIssuer)); } // http://axschema.org/namePerson/last else if (string.Equals(attribute.Key, OpenIdAuthenticationConstants.Attributes.Lastname, StringComparison.Ordinal)) { identity.AddClaim(new Claim(ClaimTypes.Surname, attribute.Value, ClaimValueTypes.String, Options.ClaimsIssuer)); } } // Create a ClaimTypes.Name claim using ClaimTypes.GivenName and ClaimTypes.Surname // if the http://axschema.org/namePerson attribute cannot be found in the assertion. if (!identity.HasClaim(claim => string.Equals(claim.Type, ClaimTypes.Name, StringComparison.OrdinalIgnoreCase)) && identity.HasClaim(claim => string.Equals(claim.Type, ClaimTypes.GivenName, StringComparison.OrdinalIgnoreCase)) && identity.HasClaim(claim => string.Equals(claim.Type, ClaimTypes.Surname, StringComparison.OrdinalIgnoreCase))) { identity.AddClaim(new Claim(ClaimTypes.Name, $"{identity.FindFirst(ClaimTypes.GivenName).Value} " + $"{identity.FindFirst(ClaimTypes.Surname).Value}", ClaimValueTypes.String, Options.ClaimsIssuer)); } var ticket = await CreateTicketAsync(identity, properties, message.ClaimedIdentifier, attributes); if (ticket == null) { Logger.LogInformation("The authentication process was skipped because returned a null ticket was returned."); return(AuthenticateResult.Skip()); } return(AuthenticateResult.Success(ticket)); }
protected override async Task <bool> HandleUnauthorizedAsync(ChallengeContext context) { var properties = new AuthenticationProperties(context.Properties); var configuration = await Options.ConfigurationManager.GetConfigurationAsync(Context.RequestAborted); if (configuration == null) { throw new InvalidOperationException("The OpenID 2.0 authentication middleware was unable to retrieve " + "the provider configuration from the OpenID 2.0 authentication server."); } if (string.IsNullOrEmpty(configuration.AuthenticationEndpoint)) { throw new InvalidOperationException("The OpenID 2.0 authentication middleware was unable to retrieve " + "the authentication endpoint address from the discovery document."); } // Determine the realm using the current address // if one has not been explicitly provided; var realm = Options.Realm; if (string.IsNullOrEmpty(realm)) { realm = Request.Scheme + "://" + Request.Host + OriginalPathBase; } // Use the current address as the final location where the user agent // will be redirected to if one has not been explicitly provided. if (string.IsNullOrEmpty(properties.RedirectUri)) { properties.RedirectUri = Request.Scheme + "://" + Request.Host + OriginalPathBase + Request.Path + Request.QueryString; } // Store the return_to parameter for later comparison. properties.Items[OpenIdAuthenticationConstants.Properties.ReturnTo] = Request.Scheme + "://" + Request.Host + OriginalPathBase + Options.CallbackPath; // Generate a new anti-forgery token. GenerateCorrelationId(properties); // Create a new message containing the OpenID 2.0 request parameters. // See http://openid.net/specs/openid-authentication-2_0.html#requesting_authentication var message = new OpenIdAuthenticationMessage { ClaimedIdentifier = "http://specs.openid.net/auth/2.0/identifier_select", Identity = "http://specs.openid.net/auth/2.0/identifier_select", Mode = OpenIdAuthenticationConstants.Modes.CheckIdSetup, Namespace = OpenIdAuthenticationConstants.Namespaces.OpenId, Realm = realm, ReturnTo = QueryHelpers.AddQueryString( uri: properties.Items[OpenIdAuthenticationConstants.Properties.ReturnTo], name: OpenIdAuthenticationConstants.Parameters.State, value: Options.StateDataFormat.Protect(properties)) }; if (Options.Attributes.Count != 0) { // openid.ns.ax (http://openid.net/srv/ax/1.0) message.SetParameter( prefix: OpenIdAuthenticationConstants.Prefixes.Namespace, name: OpenIdAuthenticationConstants.Aliases.Ax, value: OpenIdAuthenticationConstants.Namespaces.Ax); // openid.ax.mode (fetch_request) message.SetParameter( prefix: OpenIdAuthenticationConstants.Prefixes.Ax, name: OpenIdAuthenticationConstants.Parameters.Mode, value: OpenIdAuthenticationConstants.Modes.FetchRequest); foreach (var attribute in Options.Attributes) { message.SetParameter( prefix: OpenIdAuthenticationConstants.Prefixes.Ax, name: $"{OpenIdAuthenticationConstants.Prefixes.Type}.{attribute.Key}", value: attribute.Value); } // openid.ax.required message.SetParameter( prefix: OpenIdAuthenticationConstants.Prefixes.Ax, name: OpenIdAuthenticationConstants.Parameters.Required, value: string.Join(",", Options.Attributes.Select(attribute => attribute.Key))); } var address = QueryHelpers.AddQueryString(configuration.AuthenticationEndpoint, message.Parameters); Response.Redirect(address); return(true); }