Browse Source

Use the OpenIddict client in the server samples and enable GitHub login via Identity

pull/1460/head
Kévin Chalet 4 years ago
parent
commit
fcf3f92fe2
  1. 30
      sandbox/OpenIddict.Sandbox.AspNet.Client/Controllers/AuthenticationController.cs
  2. 4
      sandbox/OpenIddict.Sandbox.AspNet.Client/OpenIddict.Sandbox.AspNet.Client.csproj
  3. 4
      sandbox/OpenIddict.Sandbox.AspNet.Client/Startup.cs
  4. 2
      sandbox/OpenIddict.Sandbox.AspNet.Client/Views/Home/Index.cshtml
  5. 112
      sandbox/OpenIddict.Sandbox.AspNet.Server/Controllers/AuthenticationController.cs
  6. 49
      sandbox/OpenIddict.Sandbox.AspNet.Server/Controllers/AuthorizationController.cs
  7. 4
      sandbox/OpenIddict.Sandbox.AspNet.Server/OpenIddict.Sandbox.AspNet.Server.csproj
  8. 37
      sandbox/OpenIddict.Sandbox.AspNet.Server/Startup.cs
  9. 39
      sandbox/OpenIddict.Sandbox.AspNetCore.Client/Controllers/AuthenticationController.cs
  10. 5
      sandbox/OpenIddict.Sandbox.AspNetCore.Client/OpenIddict.Sandbox.AspNetCore.Client.csproj
  11. 2
      sandbox/OpenIddict.Sandbox.AspNetCore.Client/Startup.cs
  12. 2
      sandbox/OpenIddict.Sandbox.AspNetCore.Client/Views/Home/Index.cshtml
  13. 117
      sandbox/OpenIddict.Sandbox.AspNetCore.Server/Controllers/AuthenticationController.cs
  14. 70
      sandbox/OpenIddict.Sandbox.AspNetCore.Server/Controllers/AuthorizationController.cs
  15. 5
      sandbox/OpenIddict.Sandbox.AspNetCore.Server/OpenIddict.Sandbox.AspNetCore.Server.csproj
  16. 34
      sandbox/OpenIddict.Sandbox.AspNetCore.Server/Startup.cs
  17. 4
      src/OpenIddict.Client/OpenIddictClientBuilder.cs

30
sandbox/OpenIddict.Sandbox.AspNet.Client/Controllers/AuthenticationController.cs

@ -22,9 +22,9 @@ namespace OpenIddict.Sandbox.AspNet.Client.Controllers
var issuer = provider switch
{
"local" => "https://localhost:44349/",
"github" => "https://github.com/",
"google" => "https://accounts.google.com/",
"local" or "local-github" => "https://localhost:44349/",
"github" => "https://github.com/",
"google" => "https://accounts.google.com/",
_ => null
};
@ -45,6 +45,17 @@ namespace OpenIddict.Sandbox.AspNet.Client.Controllers
RedirectUri = Url.IsLocalUrl(returnUrl) ? returnUrl : "/"
};
// 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 (provider is "local-github")
{
// Note: the OWIN host requires appending the #string suffix to indicate
// that the "identity_provider" property is a public string parameter.
properties.Dictionary["identity_provider#string"] = "github";
}
// Ask the OpenIddict client middleware to redirect the user agent to the identity provider.
context.Authentication.Challenge(properties, OpenIddictClientOwinDefaults.AuthenticationType);
return new EmptyResult();
@ -69,7 +80,7 @@ namespace OpenIddict.Sandbox.AspNet.Client.Controllers
// 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 essentials claims in an
// * 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.
//
@ -136,16 +147,15 @@ namespace OpenIddict.Sandbox.AspNet.Client.Controllers
nameType: ClaimTypes.Name,
roleType: ClaimTypes.Role);
// If needed, the tokens returned by the authorization server can be stored in the authentication cookie.
var properties = new AuthenticationProperties(new Dictionary<string, string>
{
[Tokens.BackchannelAccessToken] = GetProperty(result.Properties, Tokens.BackchannelAccessToken),
[Tokens.RefreshToken] = GetProperty(result.Properties, Tokens.RefreshToken)
})
var properties = new AuthenticationProperties
{
RedirectUri = result.Properties.RedirectUri
};
// If needed, the tokens returned by the authorization server can be stored in the authentication cookie.
properties.Dictionary[Tokens.BackchannelAccessToken] = GetProperty(result.Properties, Tokens.BackchannelAccessToken);
properties.Dictionary[Tokens.RefreshToken] = GetProperty(result.Properties, Tokens.RefreshToken);
context.Authentication.SignIn(properties, identity);
return Redirect(properties.RedirectUri);

4
sandbox/OpenIddict.Sandbox.AspNet.Client/OpenIddict.Sandbox.AspNet.Client.csproj

@ -15,11 +15,9 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\OpenIddict.Client.Owin\OpenIddict.Client.Owin.csproj" />
<ProjectReference Include="..\..\src\OpenIddict.Client.SystemNetHttp\OpenIddict.Client.SystemNetHttp.csproj" />
<ProjectReference Include="..\..\src\OpenIddict.Client.WebIntegration\OpenIddict.Client.WebIntegration.csproj" />
<ProjectReference Include="..\..\src\OpenIddict.EntityFramework\OpenIddict.EntityFramework.csproj" />
<ProjectReference Include="..\..\src\OpenIddict.Quartz\OpenIddict.Quartz.csproj" />
<ProjectReference Include="..\..\src\OpenIddict.Owin\OpenIddict.Owin.csproj" />
</ItemGroup>
<ItemGroup>

4
sandbox/OpenIddict.Sandbox.AspNet.Client/Startup.cs

@ -29,7 +29,7 @@ namespace OpenIddict.Sandbox.AspNet.Client
SlidingExpiration = false
});
// Register the OpenIddict client middleware.
// Register the OpenIddict middleware.
app.UseMiddlewareFromContainer<OpenIddictClientOwinMiddleware>();
// Configure ASP.NET MVC 5.2 to use Autofac when activating controller instances.
@ -68,7 +68,7 @@ namespace OpenIddict.Sandbox.AspNet.Client
{
// Enable the redirection endpoint needed to handle the callback stage.
//
// Note: to prevent mix-up attacks, it's recommended to use a unique redirection endpoint
// Note: to mitigate mix-up attacks, it's recommended to use a unique redirection endpoint
// address per provider, unless all the registered providers support returning an "iss"
// parameter containing their URL as part of authorization responses. For more information,
// see https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#section-4.4.

2
sandbox/OpenIddict.Sandbox.AspNet.Client/Views/Home/Index.cshtml

@ -37,6 +37,8 @@
<h1>Welcome, anonymous</h1>
@Html.ActionLink("Sign in using the local OIDC server", "Login", "Authentication",
new { provider = "local" }, new { @class = "btn btn-lg btn-success" })
@Html.ActionLink("Sign in using the local OIDC server (using GitHub delegation)", "Login", "Authentication",
new { provider = "local-github" }, new { @class = "btn btn-lg btn-success" })
@Html.ActionLink("Sign in using GitHub", "Login", "Authentication",
new { provider = "github" }, new { @class = "btn btn-lg btn-success" })
@Html.ActionLink("Sign in using Google", "Login", "Authentication",

112
sandbox/OpenIddict.Sandbox.AspNet.Server/Controllers/AuthenticationController.cs

@ -0,0 +1,112 @@
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.AspNet.Identity;
using Microsoft.Owin.Security;
using OpenIddict.Client.Owin;
using static OpenIddict.Client.Owin.OpenIddictClientOwinConstants;
namespace OpenIddict.Sandbox.AspNet.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.
[AcceptVerbs("GET", "POST"), Route("~/signin-{provider}")]
public async Task<ActionResult> Callback()
{
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 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.Identity is not ClaimsIdentity { 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 = new List<Claim>(result.Identity.Claims
.Select(claim => claim switch
{
// Note: when using external authentication providers with ASP.NET Core Identity,
// the "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier" claim
// - which is not configurable in Identity - MUST be used to store the user identifier.
{ Type: "id", Issuer: "https://github.com/" }
=> new Claim(ClaimTypes.NameIdentifier, claim.Value, claim.ValueType, claim.Issuer),
_ => claim
})
.Where(claim => claim switch
{
// Preserve the "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier" claim.
{ Type: ClaimTypes.NameIdentifier } => true,
// Applications that use multiple client registrations can filter claims based on the issuer.
{ Type: "bio", Issuer: "https://github.com/" } => true,
// Don't preserve the other claims.
_ => false
}));
// The antiforgery components require both the nameidentifier and identityprovider claims
// so the latter is manually added using the issuer identity resolved from the remote server.
claims.Add(new Claim("http://schemas.microsoft.com/accesscontrolservice/2010/07/claims/identityprovider", claims[0].Issuer));
var identity = new ClaimsIdentity(claims,
authenticationType: DefaultAuthenticationTypes.ExternalCookie,
nameType: ClaimTypes.Name,
roleType: ClaimTypes.Role);
var properties = new AuthenticationProperties
{
RedirectUri = result.Properties.RedirectUri
};
// If needed, the tokens returned by the authorization server can be stored in the authentication cookie.
properties.Dictionary[Tokens.BackchannelAccessToken] = GetProperty(result.Properties, Tokens.BackchannelAccessToken);
properties.Dictionary[Tokens.RefreshToken] = GetProperty(result.Properties, Tokens.RefreshToken);
context.Authentication.SignIn(properties, identity);
return Redirect(properties.RedirectUri);
static string GetProperty(AuthenticationProperties properties, string name)
=> properties.Dictionary.TryGetValue(name, out var value) ? value : string.Empty;
}
}
}

49
sandbox/OpenIddict.Sandbox.AspNet.Server/Controllers/AuthorizationController.cs

@ -14,9 +14,10 @@ using System.Web.Mvc;
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.Owin;
using Microsoft.Owin.Security;
using OpenIddict.Abstractions;
using OpenIddict.Client.Owin;
using OpenIddict.Sandbox.AspNet.Server.Helpers;
using OpenIddict.Sandbox.AspNet.Server.ViewModels.Authorization;
using OpenIddict.Abstractions;
using OpenIddict.Server.Owin;
using Owin;
using static OpenIddict.Abstractions.OpenIddictConstants;
@ -53,8 +54,52 @@ namespace OpenIddict.Sandbox.AspNet.Server.Controllers
if (result?.Identity == null || (request.MaxAge != null && result.Properties?.IssuedUtc != null &&
DateTimeOffset.UtcNow - result.Properties.IssuedUtc > TimeSpan.FromSeconds(request.MaxAge.Value)))
{
context.Authentication.Challenge(DefaultAuthenticationTypes.ApplicationCookie);
// For applications that want to allow the client to select the external authentication provider
// that will be used to authenticate the user, the identity_provider parameter can be used for that.
if (!string.IsNullOrEmpty(request.IdentityProvider))
{
var issuer = request.IdentityProvider switch
{
"github" => "https://github.com/",
_ => null
};
if (string.IsNullOrEmpty(issuer))
{
context.Authentication.Challenge(
authenticationTypes: OpenIddictServerOwinDefaults.AuthenticationType,
properties: new AuthenticationProperties(new Dictionary<string, string>
{
[OpenIddictServerOwinConstants.Properties.Error] = Errors.InvalidRequest,
[OpenIddictServerOwinConstants.Properties.ErrorDescription] =
"The specified identity provider is not valid."
}));
return new EmptyResult();
}
var properties = new AuthenticationProperties(new Dictionary<string, string>
{
// Note: when only one client is registered in the client options,
// setting the issuer property is not required and can be omitted.
[OpenIddictClientOwinConstants.Properties.Issuer] = issuer
})
{
// Once the callback is handled, redirect the user agent to the ASP.NET Identity
// page responsible for showing the external login confirmation form if necessary.
RedirectUri = Url.Action("ExternalLoginCallback", "Account", new
{
ReturnUrl = Request.RawUrl
})
};
// Ask the OpenIddict client middleware to redirect the user agent to the identity provider.
context.Authentication.Challenge(properties, OpenIddictClientOwinDefaults.AuthenticationType);
return new EmptyResult();
}
context.Authentication.Challenge(DefaultAuthenticationTypes.ApplicationCookie);
return new EmptyResult();
}

4
sandbox/OpenIddict.Sandbox.AspNet.Server/OpenIddict.Sandbox.AspNet.Server.csproj

@ -15,11 +15,9 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\OpenIddict.Owin\OpenIddict.Owin.csproj" />
<ProjectReference Include="..\..\src\OpenIddict.EntityFramework\OpenIddict.EntityFramework.csproj" />
<ProjectReference Include="..\..\src\OpenIddict.MongoDb\OpenIddict.MongoDb.csproj" />
<ProjectReference Include="..\..\src\OpenIddict.Server.Owin\OpenIddict.Server.Owin.csproj" />
<ProjectReference Include="..\..\src\OpenIddict.Validation.Owin\OpenIddict.Validation.Owin.csproj" />
<ProjectReference Include="..\..\src\OpenIddict.Validation.ServerIntegration\OpenIddict.Validation.ServerIntegration.csproj" />
</ItemGroup>
<ItemGroup>

37
sandbox/OpenIddict.Sandbox.AspNet.Server/Startup.cs

@ -9,6 +9,7 @@ using Autofac.Integration.WebApi;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Owin;
using OpenIddict.Abstractions;
using OpenIddict.Client.Owin;
using OpenIddict.Sandbox.AspNet.Server.Models;
using OpenIddict.Server.Owin;
using OpenIddict.Validation.Owin;
@ -29,7 +30,8 @@ namespace OpenIddict.Sandbox.AspNet.Server
// Register the Autofac scope injector middleware.
app.UseAutofacLifetimeScopeInjector(container);
// Register the two OpenIddict server/validation middleware.
// Register the OpenIddict middleware.
app.UseMiddlewareFromContainer<OpenIddictClientOwinMiddleware>();
app.UseMiddlewareFromContainer<OpenIddictServerOwinMiddleware>();
app.UseMiddlewareFromContainer<OpenIddictValidationOwinMiddleware>();
@ -118,6 +120,39 @@ namespace OpenIddict.Sandbox.AspNet.Server
// .UseDatabase(new MongoClient().GetDatabase("openiddict"));
})
// Register the OpenIddict client components.
.AddClient(options =>
{
// Enable the redirection endpoint needed to handle the callback stage.
//
// Note: to mitigate mix-up attacks, it's recommended to use a unique redirection endpoint
// address per provider, unless all the registered providers support returning an "iss"
// parameter containing their URL as part of authorization responses. For more information,
// see https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#section-4.4.
options.SetRedirectionEndpointUris("/signin-github");
// Register the signing and encryption credentials used to protect
// sensitive data like the state tokens produced by OpenIddict.
options.AddDevelopmentEncryptionCertificate()
.AddDevelopmentSigningCertificate();
// Register the OWIN host and configure the OWIN-specific options.
options.UseOwin()
.EnableRedirectionEndpointPassthrough();
// Register the System.Net.Http integration.
options.UseSystemNetHttp();
// Register the Web providers integrations.
options.UseWebProviders()
.AddGitHub(new()
{
ClientId = "c4ade52327b01ddacff3",
ClientSecret = "da6bed851b75e317bf6b2cb67013679d9467c122",
RedirectUri = new Uri("https://localhost:44349/signin-github", UriKind.Absolute)
});
})
// Register the OpenIddict server components.
.AddServer(options =>
{

39
sandbox/OpenIddict.Sandbox.AspNetCore.Client/Controllers/AuthenticationController.cs

@ -14,10 +14,10 @@ public class AuthenticationController : Controller
{
var issuer = provider switch
{
"local" => "https://localhost:44395/",
"github" => "https://github.com/",
"google" => "https://accounts.google.com/",
"reddit" => "https://www.reddit.com/",
"local" or "local-github" => "https://localhost:44395/",
"github" => "https://github.com/",
"google" => "https://accounts.google.com/",
"reddit" => "https://www.reddit.com/",
_ => null
};
@ -38,6 +38,15 @@ public class AuthenticationController : Controller
RedirectUri = Url.IsLocalUrl(returnUrl) ? returnUrl : "/"
};
// 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 (provider is "local-github")
{
properties.Parameters["identity_provider"] = "github";
}
// Ask the OpenIddict client middleware to redirect the user agent to the identity provider.
return Challenge(properties, OpenIddictClientAspNetCoreDefaults.AuthenticationScheme);
}
@ -59,7 +68,7 @@ public class AuthenticationController : Controller
// 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 essentials claims in an
// * 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.
//
@ -87,7 +96,7 @@ public class AuthenticationController : Controller
//
// 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.Principal.Claims
var claims = new List<Claim>(result.Principal.Claims
.Select(claim => claim switch
{
// Applications can map non-standard claims issued by specific issuers to a standard equivalent.
@ -106,16 +115,21 @@ public class AuthenticationController : Controller
// Don't preserve the other claims.
_ => false
});
}));
var identity = new ClaimsIdentity(claims,
authenticationType: CookieAuthenticationDefaults.AuthenticationScheme,
nameType: Claims.Name,
roleType: Claims.Role);
var properties = new AuthenticationProperties
{
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 can be filtered out before creating the cookie.
var tokens = result.Properties.GetTokens().Where(token => token switch
properties.StoreTokens(result.Properties.GetTokens().Where(token => token switch
{
// Preserve the access and refresh tokens returned in the token response, if available.
{
@ -125,14 +139,7 @@ public class AuthenticationController : Controller
// Ignore the other tokens.
_ => false
});
var properties = new AuthenticationProperties
{
RedirectUri = result.Properties.RedirectUri
};
properties.StoreTokens(tokens);
}));
// Note: "return SignIn(...)" cannot be directly used in this case, as the cookies handler doesn't allow
// redirecting from an endpoint that doesn't match the path set in CookieAuthenticationOptions.LoginPath.

5
sandbox/OpenIddict.Sandbox.AspNetCore.Client/OpenIddict.Sandbox.AspNetCore.Client.csproj

@ -7,10 +7,7 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\OpenIddict.Client.AspNetCore\OpenIddict.Client.AspNetCore.csproj" />
<ProjectReference Include="..\..\src\OpenIddict.Client.DataProtection\OpenIddict.Client.DataProtection.csproj" />
<ProjectReference Include="..\..\src\OpenIddict.Client.SystemNetHttp\OpenIddict.Client.SystemNetHttp.csproj" />
<ProjectReference Include="..\..\src\OpenIddict.Client.WebIntegration\OpenIddict.Client.WebIntegration.csproj" />
<ProjectReference Include="..\..\src\OpenIddict.AspNetCore\OpenIddict.AspNetCore.csproj" />
<ProjectReference Include="..\..\src\OpenIddict.EntityFrameworkCore\OpenIddict.EntityFrameworkCore.csproj" />
<ProjectReference Include="..\..\src\OpenIddict.Quartz\OpenIddict.Quartz.csproj" />
</ItemGroup>

2
sandbox/OpenIddict.Sandbox.AspNetCore.Client/Startup.cs

@ -76,7 +76,7 @@ public class Startup
{
// Enable the redirection endpoint needed to handle the callback stage.
//
// Note: to prevent mix-up attacks, it's recommended to use a unique redirection endpoint
// Note: to mitigate mix-up attacks, it's recommended to use a unique redirection endpoint
// address per provider, unless all the registered providers support returning an "iss"
// parameter containing their URL as part of authorization responses. For more information,
// see https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#section-4.4.

2
sandbox/OpenIddict.Sandbox.AspNetCore.Client/Views/Home/Index.cshtml

@ -33,6 +33,8 @@
<h1>Welcome, anonymous</h1>
<a class="btn btn-lg btn-success" asp-controller="Authentication"
asp-action="Login" asp-route-provider="local">Sign in using the local OIDC server</a>
<a class="btn btn-lg btn-success" asp-controller="Authentication"
asp-action="Login" asp-route-provider="local-github">Sign in using the local OIDC server (using GitHub delegation)</a>
<a class="btn btn-lg btn-success" asp-controller="Authentication"
asp-action="Login" asp-route-provider="github">Sign in using GitHub</a>
<a class="btn btn-lg btn-success" asp-controller="Authentication"

117
sandbox/OpenIddict.Sandbox.AspNetCore.Server/Controllers/AuthenticationController.cs

@ -0,0 +1,117 @@
using System.Security.Claims;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using OpenIddict.Client.AspNetCore;
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("~/signin-{provider}"), HttpPost("~/signin-{provider}")]
public async Task<ActionResult> Callback()
{
// 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.Identity is not ClaimsIdentity { 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 = new List<Claim>(result.Principal.Claims
.Select(claim => claim switch
{
// Note: when using external authentication providers with ASP.NET Core Identity,
// the "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier" claim
// - which is not configurable in Identity - MUST be used to store the user identifier.
{ Type: "id", Issuer: "https://github.com/" }
=> new Claim(ClaimTypes.NameIdentifier, claim.Value, claim.ValueType, claim.Issuer),
_ => claim
})
.Where(claim => claim switch
{
// Preserve the "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier" claim.
{ Type: ClaimTypes.NameIdentifier } => true,
// Applications that use multiple client registrations can filter claims based on the issuer.
{ Type: "bio", Issuer: "https://github.com/" } => true,
// Don't preserve the other claims.
_ => false
}));
// Note: when using external authentication providers with ASP.NET Core Identity,
// the "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier" claim
// - which is not configurable in Identity - MUST be used to store the user identifier.
var identity = new ClaimsIdentity(claims,
authenticationType: IdentityConstants.ExternalScheme,
nameType: ClaimTypes.NameIdentifier,
roleType: ClaimTypes.Role);
var properties = new AuthenticationProperties
{
RedirectUri = result.Properties.RedirectUri
};
// Store the identity of the external provider in the authentication properties to allow
// ASP.NET Core Identity to resolve it when returning the external login confirmation form.
properties.Items["LoginProvider"] = claims[0].Issuer;
// 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 can be 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
}));
// Note: "return SignIn(...)" cannot be directly used in this case, as the cookies handler doesn't allow
// redirecting from an endpoint that doesn't match the path set in CookieAuthenticationOptions.LoginPath.
// For more information about this restriction, visit https://github.com/dotnet/aspnetcore/issues/36934.
await HttpContext.SignInAsync(IdentityConstants.ExternalScheme, new ClaimsPrincipal(identity), properties);
return Redirect(properties.RedirectUri);
}
}

70
sandbox/OpenIddict.Sandbox.AspNetCore.Server/Controllers/AuthorizationController.cs

@ -11,10 +11,11 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Primitives;
using OpenIddict.Abstractions;
using OpenIddict.Client.AspNetCore;
using OpenIddict.Sandbox.AspNetCore.Server.Helpers;
using OpenIddict.Sandbox.AspNetCore.Server.Models;
using OpenIddict.Sandbox.AspNetCore.Server.ViewModels.Authorization;
using OpenIddict.Abstractions;
using OpenIddict.Server.AspNetCore;
using static OpenIddict.Abstractions.OpenIddictConstants;
@ -54,11 +55,15 @@ public class AuthorizationController : Controller
var request = HttpContext.GetOpenIddictServerRequest() ??
throw new InvalidOperationException("The OpenID Connect request cannot be retrieved.");
// Retrieve the user principal stored in the authentication cookie.
// If a max_age parameter was provided, ensure that the cookie is not too old.
// If the user principal can't be extracted or the cookie is too old, redirect the user to the login page.
// Try to retrieve the user principal stored in the authentication cookie and redirect
// the user agent to the login page (or to an external provider) in the following cases:
//
// - If the user principal can't be extracted or the cookie is too old.
// - If prompt=login was specified by the client application.
// - If a max_age parameter was provided and the authentication cookie is not considered "fresh" enough.
var result = await HttpContext.AuthenticateAsync(IdentityConstants.ApplicationScheme);
if (result == null || !result.Succeeded || (request.MaxAge != null && result.Properties?.IssuedUtc != null &&
if (result == null || !result.Succeeded || request.HasPrompt(Prompts.Login) ||
(request.MaxAge != null && result.Properties?.IssuedUtc != null &&
DateTimeOffset.UtcNow - result.Properties.IssuedUtc > TimeSpan.FromSeconds(request.MaxAge.Value)))
{
// If the client application requested promptless authentication,
@ -74,19 +79,6 @@ public class AuthorizationController : Controller
}));
}
return Challenge(
authenticationSchemes: IdentityConstants.ApplicationScheme,
properties: new AuthenticationProperties
{
RedirectUri = Request.PathBase + Request.Path + QueryString.Create(
Request.HasFormContentType ? Request.Form.ToList() : Request.Query.ToList())
});
}
// If prompt=login was specified by the client application,
// immediately return the user agent to the login page.
if (request.HasPrompt(Prompts.Login))
{
// To avoid endless login -> authorization redirects, the prompt=login flag
// is removed from the authorization request payload before redirecting the user.
var prompt = string.Join(" ", request.GetPrompts().Remove(Prompts.Login));
@ -97,6 +89,48 @@ public class AuthorizationController : Controller
parameters.Add(KeyValuePair.Create(Parameters.Prompt, new StringValues(prompt)));
// For applications that want to allow the client to select the external authentication provider
// that will be used to authenticate the user, the identity_provider parameter can be used for that.
if (!string.IsNullOrEmpty(request.IdentityProvider))
{
var issuer = request.IdentityProvider switch
{
"github" => "https://github.com/",
_ => null
};
if (string.IsNullOrEmpty(issuer))
{
return Forbid(
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
properties: new AuthenticationProperties(new Dictionary<string, string>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidRequest,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] =
"The specified identity provider is not valid."
}));
}
var properties = new AuthenticationProperties(new Dictionary<string, string>
{
// Note: when only one client is registered in the client options,
// setting the issuer property is not required and can be omitted.
[OpenIddictClientAspNetCoreConstants.Properties.Issuer] = issuer
})
{
// Once the callback is handled, redirect the user agent to the ASP.NET Identity
// page responsible for showing the external login confirmation form if necessary.
RedirectUri = Url.Action("ExternalLoginCallback", "Account", new
{
ReturnUrl = Request.PathBase + Request.Path + QueryString.Create(parameters)
})
};
// Ask the OpenIddict client middleware to redirect the user agent to the identity provider.
return Challenge(properties, OpenIddictClientAspNetCoreDefaults.AuthenticationScheme);
}
return Challenge(
authenticationSchemes: IdentityConstants.ApplicationScheme,
properties: new AuthenticationProperties

5
sandbox/OpenIddict.Sandbox.AspNetCore.Server/OpenIddict.Sandbox.AspNetCore.Server.csproj

@ -9,13 +9,10 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\OpenIddict.AspNetCore\OpenIddict.AspNetCore.csproj" />
<ProjectReference Include="..\..\src\OpenIddict.EntityFrameworkCore\OpenIddict.EntityFrameworkCore.csproj" />
<ProjectReference Include="..\..\src\OpenIddict.MongoDb\OpenIddict.MongoDb.csproj" />
<ProjectReference Include="..\..\src\OpenIddict.Quartz\OpenIddict.Quartz.csproj" />
<ProjectReference Include="..\..\src\OpenIddict.Server.AspNetCore\OpenIddict.Server.AspNetCore.csproj" />
<ProjectReference Include="..\..\src\OpenIddict.Server.DataProtection\OpenIddict.Server.DataProtection.csproj" />
<ProjectReference Include="..\..\src\OpenIddict.Validation.AspNetCore\OpenIddict.Validation.AspNetCore.csproj" />
<ProjectReference Include="..\..\src\OpenIddict.Validation.ServerIntegration\OpenIddict.Validation.ServerIntegration.csproj" />
</ItemGroup>
<ItemGroup>

34
sandbox/OpenIddict.Sandbox.AspNetCore.Server/Startup.cs

@ -76,6 +76,40 @@ public class Startup
options.UseQuartz();
})
// Register the OpenIddict client components.
.AddClient(options =>
{
// Enable the redirection endpoint needed to handle the callback stage.
//
// Note: to mitigate mix-up attacks, it's recommended to use a unique redirection endpoint
// address per provider, unless all the registered providers support returning an "iss"
// parameter containing their URL as part of authorization responses. For more information,
// see https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#section-4.4.
options.SetRedirectionEndpointUris("/signin-github");
// Register the signing and encryption credentials used to protect
// sensitive data like the state tokens produced by OpenIddict.
options.AddDevelopmentEncryptionCertificate()
.AddDevelopmentSigningCertificate();
// Register the ASP.NET Core host and configure the ASP.NET Core-specific options.
options.UseAspNetCore()
.EnableStatusCodePagesIntegration()
.EnableRedirectionEndpointPassthrough();
// Register the System.Net.Http integration.
options.UseSystemNetHttp();
// Register the Web providers integrations.
options.UseWebProviders()
.AddGitHub(new()
{
ClientId = "c4ade52327b01ddacff3",
ClientSecret = "da6bed851b75e317bf6b2cb67013679d9467c122",
RedirectUri = new Uri("https://localhost:44395/signin-github", UriKind.Absolute)
});
})
// Register the OpenIddict server components.
.AddServer(options =>
{

4
src/OpenIddict.Client/OpenIddictClientBuilder.cs

@ -975,7 +975,7 @@ public class OpenIddictClientBuilder
/// If an empty array is specified, the endpoint will be considered disabled.
/// </summary>
/// <remarks>
/// Note: to prevent mix-up attacks, it's recommended to use a unique redirection endpoint
/// Note: to mitigate mix-up attacks, it's recommended to use a unique redirection endpoint
/// address per provider, unless all the registered providers support returning an "iss"
/// parameter containing their URL as part of authorization responses. For more information,
/// see https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#section-4.4.
@ -997,7 +997,7 @@ public class OpenIddictClientBuilder
/// If an empty array is specified, the endpoint will be considered disabled.
/// </summary>
/// <remarks>
/// Note: to prevent mix-up attacks, it's recommended to use a unique redirection endpoint
/// Note: to mitigate mix-up attacks, it's recommended to use a unique redirection endpoint
/// address per provider, unless all the registered providers support returning an "iss"
/// parameter containing their URL as part of authorization responses. For more information,
/// see https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#section-4.4.

Loading…
Cancel
Save