Created
May 24, 2024 09:26
-
-
Save newpost/3759c4409af8296eb4186e2a5af24034 to your computer and use it in GitHub Desktop.
keycloak 例子
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Licensed to the .NET Foundation under one or more agreements. | |
// The .NET Foundation licenses this file to you under the MIT license. | |
using System.Globalization; | |
using System.Net.Http; | |
using System.Security.Claims; | |
using System.Text.Encodings.Web; | |
using System.Text.Json; | |
using Microsoft.AspNetCore.Authentication; | |
using Microsoft.AspNetCore.Authentication.Cookies; | |
using Microsoft.AspNetCore.Authentication.OAuth.Claims; | |
using Microsoft.AspNetCore.Authentication.OpenIdConnect; | |
using Microsoft.Extensions.Options; | |
using Microsoft.IdentityModel.Protocols.OpenIdConnect; | |
namespace OpenIdConnectSample; | |
public class Startup | |
{ | |
public Startup(IConfiguration config, IWebHostEnvironment env) | |
{ | |
Configuration = config; | |
Environment = env; | |
} | |
public IConfiguration Configuration { get; set; } | |
public IWebHostEnvironment Environment { get; } | |
private void CheckSameSite(HttpContext httpContext, CookieOptions options) | |
{ | |
if (options.SameSite == SameSiteMode.None) | |
{ | |
var userAgent = httpContext.Request.Headers["User-Agent"].ToString(); | |
if (DisallowsSameSiteNone(userAgent)) | |
{ | |
options.SameSite = SameSiteMode.Unspecified; | |
} | |
} | |
} | |
// TODO: Use your User Agent library of choice here. | |
public static bool DisallowsSameSiteNone(string userAgent) | |
{ | |
if (string.IsNullOrEmpty(userAgent)) | |
{ | |
return false; | |
} | |
// Cover all iOS based browsers here. This includes: | |
// - Safari on iOS 12 for iPhone, iPod Touch, iPad | |
// - WkWebview on iOS 12 for iPhone, iPod Touch, iPad | |
// - Chrome on iOS 12 for iPhone, iPod Touch, iPad | |
// All of which are broken by SameSite=None, because they use the iOS networking stack | |
if (userAgent.Contains("CPU iPhone OS 12") || userAgent.Contains("iPad; CPU OS 12")) | |
{ | |
return true; | |
} | |
// Cover Mac OS X based browsers that use the Mac OS networking stack. This includes: | |
// - Safari on Mac OS X. | |
// This does not include: | |
// - Chrome on Mac OS X | |
// Because they do not use the Mac OS networking stack. | |
if (userAgent.Contains("Macintosh; Intel Mac OS X 10_14") && | |
userAgent.Contains("Version/") && userAgent.Contains("Safari")) | |
{ | |
return true; | |
} | |
// Cover Chrome 50-69, because some versions are broken by SameSite=None, | |
// and none in this range require it. | |
// Note: this covers some pre-Chromium Edge versions, | |
// but pre-Chromium Edge does not require SameSite=None. | |
if (userAgent.Contains("Chrome/5") || userAgent.Contains("Chrome/6")) | |
{ | |
return true; | |
} | |
return false; | |
} | |
public void ConfigureServices(IServiceCollection services) | |
{ | |
services.Configure<CookiePolicyOptions>(options => | |
{ | |
options.MinimumSameSitePolicy = SameSiteMode.Unspecified; | |
options.OnAppendCookie = cookieContext => CheckSameSite(cookieContext.Context, cookieContext.CookieOptions); | |
options.OnDeleteCookie = cookieContext => CheckSameSite(cookieContext.Context, cookieContext.CookieOptions); | |
}); | |
services.AddAuthentication(sharedOptions => | |
{ | |
sharedOptions.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; | |
sharedOptions.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme; | |
}) | |
.AddCookie() | |
.AddOpenIdConnect(o => | |
{ | |
/* | |
o.ClientId = Configuration["oidc:clientid"]; | |
o.ClientSecret = Configuration["oidc:clientsecret"]; // for code flow | |
o.Authority = Configuration["oidc:authority"]; | |
*/ | |
// https://github.com/IdentityServer/IdentityServer4.Demo/blob/master/src/IdentityServer4Demo/Config.cs | |
o.ClientId = "interactive.confidential"; | |
o.ClientSecret = "secret"; // for code flow | |
o.Authority = "https://demo.duendesoftware.com/"; | |
o.ResponseType = OpenIdConnectResponseType.Code; | |
o.SaveTokens = true; | |
o.GetClaimsFromUserInfoEndpoint = true; | |
o.AccessDeniedPath = "/access-denied-from-remote"; | |
// o.MapInboundClaims = false; | |
o.ClaimsIssuer = "MyCustomIssuer"; | |
o.ClaimActions.Add(new IssuerFixupAction()); | |
// o.ClaimActions.MapAllExcept("aud", "iss", "iat", "nbf", "exp", "aio", "c_hash", "uti", "nonce"); | |
o.Events = new OpenIdConnectEvents() | |
{ | |
OnAuthenticationFailed = c => | |
{ | |
c.HandleResponse(); | |
c.Response.StatusCode = 500; | |
c.Response.ContentType = "text/plain"; | |
if (Environment.IsDevelopment()) | |
{ | |
// Debug only, in production do not share exceptions with the remote host. | |
return c.Response.WriteAsync(c.Exception.ToString()); | |
} | |
return c.Response.WriteAsync("An error occurred processing your authentication."); | |
} | |
}; | |
}); | |
} | |
public void Configure(IApplicationBuilder app, IOptionsMonitor<OpenIdConnectOptions> optionsMonitor) | |
{ | |
app.UseDeveloperExceptionPage(); | |
app.UseCookiePolicy(); // Before UseAuthentication or anything else that writes cookies. | |
app.UseAuthentication(); | |
app.Run(async context => | |
{ | |
var response = context.Response; | |
if (context.Request.Path.Equals("/signedout")) | |
{ | |
await WriteHtmlAsync(response, async res => | |
{ | |
await res.WriteAsync($"<h1>You have been signed out.</h1>"); | |
await res.WriteAsync("<a class=\"btn btn-default\" href=\"/\">Home</a>"); | |
}); | |
return; | |
} | |
if (context.Request.Path.Equals("/signout")) | |
{ | |
await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); | |
await WriteHtmlAsync(response, async res => | |
{ | |
await res.WriteAsync($"<h1>Signed out {HtmlEncode(context.User.Identity.Name)}</h1>"); | |
await res.WriteAsync("<a class=\"btn btn-default\" href=\"/\">Home</a>"); | |
}); | |
return; | |
} | |
if (context.Request.Path.Equals("/signout-remote")) | |
{ | |
// Redirects | |
await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); | |
await context.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme, new AuthenticationProperties() | |
{ | |
RedirectUri = "/signedout" | |
}); | |
return; | |
} | |
if (context.Request.Path.Equals("/access-denied-from-remote")) | |
{ | |
await WriteHtmlAsync(response, async res => | |
{ | |
await res.WriteAsync($"<h1>Access Denied error received from the remote authorization server</h1>"); | |
await res.WriteAsync("<a class=\"btn btn-default\" href=\"/\">Home</a>"); | |
}); | |
return; | |
} | |
if (context.Request.Path.Equals("/Account/AccessDenied")) | |
{ | |
await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); | |
await WriteHtmlAsync(response, async res => | |
{ | |
await res.WriteAsync($"<h1>Access Denied for user {HtmlEncode(context.User.Identity.Name)} to resource '{HtmlEncode(context.Request.Query["ReturnUrl"])}'</h1>"); | |
await res.WriteAsync("<a class=\"btn btn-default\" href=\"/signout\">Sign Out</a>"); | |
await res.WriteAsync("<a class=\"btn btn-default\" href=\"/\">Home</a>"); | |
}); | |
return; | |
} | |
// DefaultAuthenticateScheme causes User to be set | |
// var user = context.User; | |
// This is what [Authorize] calls | |
var userResult = await context.AuthenticateAsync(); | |
var user = userResult.Principal; | |
var props = userResult.Properties; | |
// This is what [Authorize(ActiveAuthenticationSchemes = OpenIdConnectDefaults.AuthenticationScheme)] calls | |
// var user = await context.AuthenticateAsync(OpenIdConnectDefaults.AuthenticationScheme); | |
// Not authenticated | |
if (user == null || !user.Identities.Any(identity => identity.IsAuthenticated)) | |
{ | |
// This is what [Authorize] calls | |
await context.ChallengeAsync(); | |
// This is what [Authorize(ActiveAuthenticationSchemes = OpenIdConnectDefaults.AuthenticationScheme)] calls | |
// await context.ChallengeAsync(OpenIdConnectDefaults.AuthenticationScheme); | |
return; | |
} | |
// Authenticated, but not authorized | |
if (context.Request.Path.Equals("/restricted") && !user.Identities.Any(identity => identity.HasClaim("special", "true"))) | |
{ | |
await context.ForbidAsync(); | |
return; | |
} | |
if (context.Request.Path.Equals("/refresh")) | |
{ | |
var refreshToken = props.GetTokenValue("refresh_token"); | |
if (string.IsNullOrEmpty(refreshToken)) | |
{ | |
await WriteHtmlAsync(response, async res => | |
{ | |
await res.WriteAsync($"No refresh_token is available.<br>"); | |
await res.WriteAsync("<a class=\"btn btn-link\" href=\"/signout\">Sign Out</a>"); | |
}); | |
return; | |
} | |
var options = optionsMonitor.Get(OpenIdConnectDefaults.AuthenticationScheme); | |
var metadata = await options.ConfigurationManager.GetConfigurationAsync(context.RequestAborted); | |
var pairs = new Dictionary<string, string>() | |
{ | |
{ "client_id", options.ClientId }, | |
{ "client_secret", options.ClientSecret }, | |
{ "grant_type", "refresh_token" }, | |
{ "refresh_token", refreshToken } | |
}; | |
var content = new FormUrlEncodedContent(pairs); | |
var tokenResponse = await options.Backchannel.PostAsync(metadata.TokenEndpoint, content, context.RequestAborted); | |
tokenResponse.EnsureSuccessStatusCode(); | |
using (var payload = JsonDocument.Parse(await tokenResponse.Content.ReadAsStringAsync())) | |
{ | |
// Persist the new acess token | |
props.UpdateTokenValue("access_token", payload.RootElement.GetString("access_token")); | |
props.UpdateTokenValue("refresh_token", payload.RootElement.GetString("refresh_token")); | |
if (payload.RootElement.TryGetProperty("expires_in", out var property) && int.TryParse(property.GetString(), out var seconds)) | |
{ | |
var expiresAt = DateTimeOffset.UtcNow + TimeSpan.FromSeconds(seconds); | |
props.UpdateTokenValue("expires_at", expiresAt.ToString("o", CultureInfo.InvariantCulture)); | |
} | |
await context.SignInAsync(user, props); | |
await WriteHtmlAsync(response, async res => | |
{ | |
await res.WriteAsync($"<h1>Refreshed.</h1>"); | |
await res.WriteAsync("<a class=\"btn btn-default\" href=\"/refresh\">Refresh tokens</a>"); | |
await res.WriteAsync("<a class=\"btn btn-default\" href=\"/\">Home</a>"); | |
await res.WriteAsync("<h2>Tokens:</h2>"); | |
await WriteTableHeader(res, new string[] { "Token Type", "Value" }, props.GetTokens().Select(token => new string[] { token.Name, token.Value })); | |
await res.WriteAsync("<h2>Payload:</h2>"); | |
await res.WriteAsync(HtmlEncoder.Default.Encode(payload.RootElement.ToString()).Replace(",", ",<br>") + "<br>"); | |
}); | |
} | |
return; | |
} | |
if (context.Request.Path.Equals("/login-challenge")) | |
{ | |
// Challenge the user authentication, and force a login prompt by overwriting the | |
// "prompt". This could be used for example to require the user to re-enter their | |
// credentials at the authentication provider, to add an extra confirmation layer. | |
await context.ChallengeAsync(OpenIdConnectDefaults.AuthenticationScheme, new OpenIdConnectChallengeProperties() | |
{ | |
Prompt = "login", | |
// it is also possible to specify different scopes, e.g. | |
// Scope = new string[] { "openid", "profile", "other" } | |
}); | |
return; | |
} | |
await WriteHtmlAsync(response, async res => | |
{ | |
await res.WriteAsync($"<h1>Hello Authenticated User {HtmlEncode(user.Identity.Name)}</h1>"); | |
await res.WriteAsync("<a class=\"btn btn-default\" href=\"/refresh\">Refresh tokens</a>"); | |
await res.WriteAsync("<a class=\"btn btn-default\" href=\"/restricted\">Restricted</a>"); | |
await res.WriteAsync("<a class=\"btn btn-default\" href=\"/login-challenge\">Login challenge</a>"); | |
await res.WriteAsync("<a class=\"btn btn-default\" href=\"/signout\">Sign Out</a>"); | |
await res.WriteAsync("<a class=\"btn btn-default\" href=\"/signout-remote\">Sign Out Remote</a>"); | |
await res.WriteAsync("<h2>Claims:</h2>"); | |
await WriteTableHeader(res, new string[] { "Claim Type", "Value" }, context.User.Claims.Select(c => new string[] { c.Type, c.Value })); | |
await res.WriteAsync("<h2>Tokens:</h2>"); | |
await WriteTableHeader(res, new string[] { "Token Type", "Value" }, props.GetTokens().Select(token => new string[] { token.Name, token.Value })); | |
}); | |
}); | |
} | |
private static async Task WriteHtmlAsync(HttpResponse response, Func<HttpResponse, Task> writeContent) | |
{ | |
var bootstrap = "<link rel=\"stylesheet\" href=\"https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css\" integrity=\"sha384-HSMxcRTRxnN+Bdg0JdbxYKrThecOKuH5zCYotlSAcp1+c8xmyTe9GYg1l9a69psu\" crossorigin=\"anonymous\">"; | |
response.ContentType = "text/html"; | |
await response.WriteAsync($"<html><head>{bootstrap}</head><body><div class=\"container\">"); | |
await writeContent(response); | |
await response.WriteAsync("</div></body></html>"); | |
} | |
private static async Task WriteTableHeader(HttpResponse response, IEnumerable<string> columns, IEnumerable<IEnumerable<string>> data) | |
{ | |
await response.WriteAsync("<table class=\"table table-condensed\">"); | |
await response.WriteAsync("<tr>"); | |
foreach (var column in columns) | |
{ | |
await response.WriteAsync($"<th>{HtmlEncode(column)}</th>"); | |
} | |
await response.WriteAsync("</tr>"); | |
foreach (var row in data) | |
{ | |
await response.WriteAsync("<tr>"); | |
foreach (var column in row) | |
{ | |
await response.WriteAsync($"<td>{HtmlEncode(column)}</td>"); | |
} | |
await response.WriteAsync("</tr>"); | |
} | |
await response.WriteAsync("</table>"); | |
} | |
private static string HtmlEncode(string content) => | |
string.IsNullOrEmpty(content) ? string.Empty : HtmlEncoder.Default.Encode(content); | |
private class IssuerFixupAction : ClaimAction | |
{ | |
public IssuerFixupAction() : base(ClaimTypes.NameIdentifier, string.Empty) { } | |
public override void Run(JsonElement userData, ClaimsIdentity identity, string issuer) | |
{ | |
var oldClaims = identity.Claims.ToList(); | |
foreach (var claim in oldClaims) | |
{ | |
identity.RemoveClaim(claim); | |
identity.AddClaim(new Claim(claim.Type, claim.Value, claim.ValueType, issuer, claim.OriginalIssuer, claim.Subject)); | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment