You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
94 lines
5.9 KiB
94 lines
5.9 KiB
using System.Security.Claims;
|
|
using Microsoft.AspNetCore.Authentication;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using OpenIddict.Abstractions;
|
|
using OpenIddict.Client.AspNetCore;
|
|
using static OpenIddict.Abstractions.OpenIddictConstants;
|
|
|
|
namespace OpenIddict.Sandbox.AspNetCore.Server.Controllers;
|
|
|
|
public class AuthenticationController : Controller
|
|
{
|
|
// 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.
|
|
[HttpGet("~/callback/login/{provider}"), HttpPost("~/callback/login/{provider}"), IgnoreAntiforgeryToken]
|
|
public async Task<ActionResult> LogInCallback()
|
|
{
|
|
// Retrieve the authorization data validated by OpenIddict as part of the callback handling.
|
|
var result = await HttpContext.AuthenticateAsync(OpenIddictClientAspNetCoreDefaults.AuthenticationScheme);
|
|
|
|
// 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 claim.
|
|
//
|
|
// Such identities cannot be used as-is to build an authentication cookie in ASP.NET Core (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.Principal is not ClaimsPrincipal { 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.
|
|
var identity = new ClaimsIdentity(authenticationType: "ExternalLogin");
|
|
|
|
// By default, OpenIddict will automatically try to map the email/name and name identifier claims from
|
|
// their standard OpenID Connect or provider-specific equivalent, if available. If needed, additional
|
|
// claims can be resolved from the external identity and copied to the final authentication cookie.
|
|
identity.SetClaim(ClaimTypes.Email, result.Principal.GetClaim(ClaimTypes.Email))
|
|
.SetClaim(ClaimTypes.Name, result.Principal.GetClaim(ClaimTypes.Name))
|
|
.SetClaim(ClaimTypes.NameIdentifier, result.Principal.GetClaim(ClaimTypes.NameIdentifier));
|
|
|
|
// Preserve the registration details to be able to resolve them later.
|
|
identity.SetClaim(Claims.Private.RegistrationId, result.Principal.GetClaim(Claims.Private.RegistrationId))
|
|
.SetClaim(Claims.Private.ProviderName, result.Principal.GetClaim(Claims.Private.ProviderName));
|
|
|
|
// Build the authentication properties based on the properties that were added when the challenge was triggered.
|
|
var properties = new AuthenticationProperties(result.Properties.Items)
|
|
{
|
|
RedirectUri = result.Properties.RedirectUri ?? "/"
|
|
};
|
|
|
|
// If needed, the tokens returned by the authorization server can be stored in the authentication cookie.
|
|
// To make cookies less heavy, tokens that are not used are filtered out before creating the cookie.
|
|
properties.StoreTokens(result.Properties.GetTokens().Where(token => token switch
|
|
{
|
|
// Preserve the access and refresh tokens returned in the token response, if available.
|
|
{
|
|
Name: OpenIddictClientAspNetCoreConstants.Tokens.BackchannelAccessToken or
|
|
OpenIddictClientAspNetCoreConstants.Tokens.RefreshToken
|
|
} => true,
|
|
|
|
// Ignore the other tokens.
|
|
_ => false
|
|
}));
|
|
|
|
// Ask the default sign-in handler to return a new cookie and redirect the
|
|
// user agent to the return URL stored in the authentication properties.
|
|
//
|
|
// For scenarios where the default sign-in handler configured in the ASP.NET Core
|
|
// authentication options shouldn't be used, a specific scheme can be specified here.
|
|
return SignIn(new ClaimsPrincipal(identity), properties);
|
|
}
|
|
}
|
|
|