using System; using System.Collections.Generic; using System.Linq; using System.Security.Claims; using System.Threading.Tasks; using System.Web; using System.Web.Mvc; using Microsoft.Owin.Security; using Microsoft.Owin.Security.Cookies; using OpenIddict.Client; using OpenIddict.Client.Owin; using static OpenIddict.Abstractions.OpenIddictConstants; namespace OpenIddict.Sandbox.AspNet.Client.Controllers; public class AuthenticationController : Controller { private readonly OpenIddictClientService _service; public AuthenticationController(OpenIddictClientService service) => _service = service; [HttpPost, Route("~/login"), ValidateAntiForgeryToken] public async Task LogIn(string provider, string returnUrl) { var context = HttpContext.GetOwinContext(); // The local authorization server sample allows the client to select the external // identity provider that will be used to eventually authenticate the user. For that, // a custom "identity_provider" parameter is sent to the authorization server so that // the user is directly redirected to GitHub (in this case, no login page is shown). if (string.Equals(provider, "Local+GitHub", StringComparison.Ordinal)) { var properties = new AuthenticationProperties(new Dictionary { // Note: when only one client is registered in the client options, // specifying the issuer URI or the provider name is not required. [OpenIddictClientOwinConstants.Properties.ProviderName] = "Local", // Note: the OWIN host requires appending the #string suffix to indicate // that the "identity_provider" property is a public string parameter. [Parameters.IdentityProvider + OpenIddictClientOwinConstants.PropertyTypes.String] = "GitHub" }) { // Only allow local return URLs to prevent open redirect attacks. RedirectUri = Url.IsLocalUrl(returnUrl) ? returnUrl : "/" }; // Ask the OpenIddict client middleware to redirect the user agent to the identity provider. context.Authentication.Challenge(properties, OpenIddictClientOwinDefaults.AuthenticationType); return new EmptyResult(); } else { // Note: OpenIddict always validates the specified provider name when handling the challenge operation, // but the provider can also be validated earlier to return an error page or a special HTTP error code. var registrations = await _service.GetClientRegistrationsAsync(); if (!registrations.Any(registration => string.Equals(registration.ProviderName, provider, StringComparison.Ordinal))) { return new HttpStatusCodeResult(400); } var properties = new AuthenticationProperties(new Dictionary { // Note: when only one client is registered in the client options, // specifying the issuer URI or the provider name is not required. [OpenIddictClientOwinConstants.Properties.ProviderName] = provider }) { // Only allow local return URLs to prevent open redirect attacks. RedirectUri = Url.IsLocalUrl(returnUrl) ? returnUrl : "/" }; // Ask the OpenIddict client middleware to redirect the user agent to the identity provider. context.Authentication.Challenge(properties, OpenIddictClientOwinDefaults.AuthenticationType); return new EmptyResult(); } } [HttpPost, Route("~/logout"), ValidateAntiForgeryToken] public async Task LogOut(string returnUrl) { var context = HttpContext.GetOwinContext(); // Retrieve the identity stored in the local authentication cookie. If it's not available, // this indicate that the user is already logged out locally (or has not logged in yet). var result = await context.Authentication.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationType); if (result is not { Identity: ClaimsIdentity { IsAuthenticated: true } identity }) { // Only allow local return URLs to prevent open redirect attacks. return Redirect(Url.IsLocalUrl(returnUrl) ? returnUrl : "/"); } // Remove the local authentication cookie before triggering a redirection to the remote server. context.Authentication.SignOut(CookieAuthenticationDefaults.AuthenticationType); // Extract the client registration identifier and retrieve the associated server configuration. // If the provider is known to support remote sign-out, ask OpenIddict to initiate a end session request. if (identity.FindFirst(Claims.Private.RegistrationId)?.Value is string identifier && await _service.GetServerConfigurationByRegistrationIdAsync(identifier) is { EndSessionEndpoint: Uri }) { var properties = new AuthenticationProperties(new Dictionary { [OpenIddictClientOwinConstants.Properties.RegistrationId] = identifier, // While not required, the specification encourages sending an id_token_hint // parameter containing an identity token returned by the server for this user. [OpenIddictClientOwinConstants.Properties.IdentityTokenHint] = result.Properties.Dictionary[OpenIddictClientOwinConstants.Tokens.BackchannelIdentityToken] }) { // Only allow local return URLs to prevent open redirect attacks. RedirectUri = Url.IsLocalUrl(returnUrl) ? returnUrl : "/" }; // Ask the OpenIddict client middleware to redirect the user agent to the identity provider. context.Authentication.SignOut(properties, OpenIddictClientOwinDefaults.AuthenticationType); return new EmptyResult(); } // Only allow local return URLs to prevent open redirect attacks. return Redirect(Url.IsLocalUrl(returnUrl) ? returnUrl : "/"); } // Note: this controller uses the same callback action for all providers // but for users who prefer using a different action per provider, // the following action can be split into separate actions. [AcceptVerbs("GET", "POST"), Route("~/callback/login/{provider}")] public async Task LogInCallback() { var context = HttpContext.GetOwinContext(); // Retrieve the authorization data validated by OpenIddict as part of the callback handling. var result = await context.Authentication.AuthenticateAsync(OpenIddictClientOwinDefaults.AuthenticationType); // Multiple strategies exist to handle OAuth 2.0/OpenID Connect callbacks, each with their pros and cons: // // * Directly using the tokens to perform the necessary action(s) on behalf of the user, which is suitable // for applications that don't need a long-term access to the user's resources or don't want to store // access/refresh tokens in a database or in an authentication cookie (which has security implications). // It is also suitable for applications that don't need to authenticate users but only need to perform // action(s) on their behalf by making API calls using the access token returned by the remote server. // // * Storing the external claims/tokens in a database (and optionally keeping the essential claims in an // authentication cookie so that cookie size limits are not hit). For the applications that use ASP.NET // Core Identity, the UserManager.SetAuthenticationTokenAsync() API can be used to store external tokens. // // Note: in this case, it's recommended to use column encryption to protect the tokens in the database. // // * Storing the external claims/tokens in an authentication cookie, which doesn't require having // a user database but may be affected by the cookie size limits enforced by most browser vendors // (e.g Safari for macOS and Safari for iOS/iPadOS enforce a per-domain 4KB limit for all cookies). // // Note: this is the approach used here, but the external claims are first filtered to only persist // a few claims like the user identifier. The same approach is used to store the access/refresh tokens. // Important: if the remote server doesn't support OpenID Connect and doesn't expose a userinfo endpoint, // result.Principal.Identity will represent an unauthenticated identity and won't contain any user claim. // // Such identities cannot be used as-is to build an authentication cookie in ASP.NET (as the // antiforgery stack requires at least a name claim to bind CSRF cookies to the user's identity) but // the access/refresh tokens can be retrieved using result.Properties.GetTokens() to make API calls. if (result is not { Identity.IsAuthenticated: true }) { throw new InvalidOperationException("The external authorization data cannot be used for authentication."); } // Build an identity based on the external claims and that will be used to create the authentication cookie. // // By default, all claims extracted during the authorization dance are available. The claims collection stored // in the cookie can be filtered out or mapped to different names depending the claim name or its issuer. var claims = result.Identity.Claims.Where(claim => claim.Type is ClaimTypes.NameIdentifier or ClaimTypes.Name // // Preserve the registration details to be able to resolve them later. // or Claims.Private.RegistrationId or Claims.Private.ProviderName // // The ASP.NET 4.x antiforgery module requires preserving the "identityprovider" claim. // or "http://schemas.microsoft.com/accesscontrolservice/2010/07/claims/identityprovider"); var identity = new ClaimsIdentity(claims, authenticationType: CookieAuthenticationDefaults.AuthenticationType, nameType: ClaimTypes.Name, roleType: ClaimTypes.Role); // Build the authentication properties based on the properties that were added when the challenge was triggered. var properties = new AuthenticationProperties(result.Properties.Dictionary .Where(item => item.Key is // Preserve the return URL. ".redirect" or // If needed, the tokens returned by the authorization server can be stored in the authentication cookie. OpenIddictClientOwinConstants.Tokens.BackchannelAccessToken or OpenIddictClientOwinConstants.Tokens.BackchannelIdentityToken or OpenIddictClientOwinConstants.Tokens.RefreshToken) .ToDictionary(pair => pair.Key, pair => pair.Value)) { // Set the creation and expiration dates of the ticket to null to decorrelate the lifetime // of the resulting authentication cookie from the lifetime of the identity token returned by // the authorization server (if applicable). In this case, the expiration date time will be // automatically computed by the cookie handler using the lifetime configured in the options. // // Applications that prefer binding the lifetime of the ticket stored in the authentication cookie // to the identity token returned by the identity provider can remove or comment these two lines: IssuedUtc = null, ExpiresUtc = null, // Note: this flag controls whether the authentication cookie that will be returned to the // browser will be treated as a session cookie (i.e destroyed when the browser is closed) // or as a persistent cookie. In both cases, the lifetime of the authentication ticket is // always stored as protected data, preventing malicious users from trying to use an // authentication cookie beyond the lifetime of the authentication ticket itself. IsPersistent = false }; context.Authentication.SignIn(properties, identity); return Redirect(properties.RedirectUri ?? "/"); } // Note: this controller uses the same callback action for all providers // but for users who prefer using a different action per provider, // the following action can be split into separate actions. [AcceptVerbs("GET", "POST"), Route("~/callback/logout/{provider}")] public async Task LogOutCallback() { var context = HttpContext.GetOwinContext(); // Retrieve the data stored by OpenIddict in the state token created when the logout was triggered. var result = await context.Authentication.AuthenticateAsync(OpenIddictClientOwinDefaults.AuthenticationType); // In this sample, the local authentication cookie is always removed before the user agent is redirected // to the authorization server. Applications that prefer delaying the removal of the local cookie can // remove the corresponding code from the logout action and remove the authentication cookie in this action. return Redirect(result?.Properties?.RedirectUri ?? "/"); } }