Last active
June 16, 2021 00:15
-
-
Save maucaro/50e21fd86bd2894e88ab552521074d8c to your computer and use it in GitHub Desktop.
Custom Authentication in a .NET Core Web API using OIDC tokens
This file contains hidden or 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
{ | |
"OidcOptions": { | |
"TrustedAudiences": [ "[YOUR_GCP_PROJECT_ID]" ], | |
"CertificatesUrl": "https://www.googleapis.com/service_accounts/v1/jwk/[email protected]" | |
}, | |
"Logging": { | |
"LogLevel": { | |
"Default": "Information", | |
"Microsoft": "Warning", | |
"Microsoft.Hosting.Lifetime": "Information" | |
} | |
}, | |
"AllowedHosts": "*" | |
} |
This file contains hidden or 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
using System; | |
using System.Security.Claims; | |
using System.Text.Encodings.Web; | |
using System.Threading.Tasks; | |
using Microsoft.AspNetCore.Authentication; | |
using Microsoft.Extensions.Logging; | |
using Microsoft.Extensions.Options; | |
using Google.Apis.Auth; | |
using System.Linq; | |
using System.Text; | |
using Microsoft.AspNetCore.WebUtilities; | |
using System.Text.Json; | |
namespace OIDCAuthentication | |
{ | |
public static class OIDCAuthenticationDefaults | |
{ | |
public const string AuthenticationScheme = "OIDCAuthentication"; | |
} | |
public static class AuthenticationBuilderExtensions | |
{ | |
public static AuthenticationBuilder AddCustomAuth(this AuthenticationBuilder builder, Action<ValidateOIDCAuthenticationSchemeOptions> configureOptions) | |
{ | |
return builder.AddScheme<ValidateOIDCAuthenticationSchemeOptions, ValidateOIDCAuthenticationHandler> | |
(OIDCAuthenticationDefaults.AuthenticationScheme, configureOptions); | |
} | |
} | |
public class ValidateOIDCAuthenticationSchemeOptions: AuthenticationSchemeOptions | |
{ | |
public SignedTokenVerificationOptions TokenVerificationOptions { get; set; } | |
} | |
public class ValidateOIDCAuthenticationHandler: AuthenticationHandler<ValidateOIDCAuthenticationSchemeOptions> | |
{ | |
public ValidateOIDCAuthenticationHandler( | |
IOptionsMonitor<ValidateOIDCAuthenticationSchemeOptions> options, | |
ILoggerFactory logger, | |
UrlEncoder encoder, | |
ISystemClock clock) | |
: base(options, logger, encoder, clock) { } | |
protected override async Task<AuthenticateResult> HandleAuthenticateAsync() | |
{ | |
if (!Request.Headers.ContainsKey("Authorization")) | |
{ | |
return AuthenticateResult.Fail("Authorization header missing"); | |
} | |
var rawToken = ExtractRawToken(Request.Headers["Authorization"].ToString()); | |
if (string.IsNullOrWhiteSpace(rawToken)) | |
{ | |
return AuthenticateResult.Fail("Bearer token missing"); | |
} | |
try | |
{ | |
// Payload returned by JsonWebSignature.VerifySignedTokenAsync does not include the email claim | |
// Will decode it using WebEncoders.Base64UrlDecode | |
// JsonWebSignature.Payload payload = await JsonWebSignature.VerifySignedTokenAsync(token, Options.TokenVerificationOptions); | |
var generalValidationTask = JsonWebSignature.VerifySignedTokenAsync(rawToken, Options.TokenVerificationOptions); | |
var payload = rawToken.Split('.').ElementAtOrDefault(1); // A token has the form 'header.payload.signature' | |
JsonDocument payloadJson = JsonDocument.Parse(Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(payload))); | |
var sub = payloadJson.RootElement.GetString("sub"); | |
var email = payloadJson.RootElement.GetString("email"); | |
if (string.IsNullOrWhiteSpace(sub) || string.IsNullOrWhiteSpace(email)) | |
{ | |
return AuthenticateResult.Fail("Error validating token: 'sub' and 'email' claims are required"); | |
} | |
var claims = new[] { | |
new Claim(ClaimTypes.NameIdentifier, sub), | |
new Claim(ClaimTypes.Email, email)}; | |
var claimsIdentity = new ClaimsIdentity(claims, nameof(ValidateOIDCAuthenticationHandler)); | |
var ticket = new AuthenticationTicket(new ClaimsPrincipal(claimsIdentity), this.Scheme.Name); | |
await generalValidationTask.ConfigureAwait(false); | |
return AuthenticateResult.Success(ticket); | |
} | |
catch(Exception ex) | |
{ | |
switch(ex) | |
{ | |
case FormatException: // Payload decoding failed - malformed input (i.e. whitespace or padding characters) | |
case ArgumentNullException: // Payload decoding failed - rawToken likely not in 'header.payload.signature' format | |
return AuthenticateResult.Fail("Error validating token: payload decoding failed"); | |
case JsonException: // Payload failed to serialize to JSON | |
return AuthenticateResult.Fail("Error validating token: converting payload to JSON failed"); | |
case InvalidJwtException: // Token failed verification | |
default: | |
return AuthenticateResult.Fail($"Error validating token: ${ex.Message}"); | |
} | |
} | |
} | |
private static string ExtractRawToken(string Header) | |
{ | |
if (string.IsNullOrWhiteSpace(Header)) | |
{ | |
return string.Empty; | |
} | |
string[] splitHeader = Header.ToString().Split(' '); | |
if (splitHeader.Length != 2) | |
{ | |
return string.Empty; | |
} | |
var scheme = splitHeader[0]; | |
var token = splitHeader[1]; | |
if (string.IsNullOrWhiteSpace(token) || scheme.ToLowerInvariant() != "bearer") | |
{ | |
return string.Empty; | |
} | |
return token; | |
} | |
} | |
} |
This file contains hidden or 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
using Microsoft.AspNetCore.Builder; | |
using Microsoft.AspNetCore.Hosting; | |
using Microsoft.Extensions.Configuration; | |
using Microsoft.Extensions.DependencyInjection; | |
using Microsoft.Extensions.Hosting; | |
using Microsoft.OpenApi.Models; | |
using Google.Apis.Auth; | |
using OIDCAuthentication; | |
namespace WebApiCustomAuthOIDC | |
{ | |
public class Startup | |
{ | |
public Startup(IConfiguration configuration) | |
{ | |
Configuration = configuration; | |
} | |
public IConfiguration Configuration { get; } | |
// This method gets called by the runtime. Use this method to add services to the container. | |
public void ConfigureServices(IServiceCollection services) | |
{ | |
services.AddAuthentication(options => | |
{ | |
options.DefaultAuthenticateScheme = OIDCAuthenticationDefaults.AuthenticationScheme; | |
options.DefaultChallengeScheme = OIDCAuthenticationDefaults.AuthenticationScheme; | |
}) | |
.AddCustomAuth(options => { | |
SignedTokenVerificationOptions tokenOptions = new SignedTokenVerificationOptions(); | |
Configuration.GetSection("OidcOptions").Bind(tokenOptions); | |
options.TokenVerificationOptions = tokenOptions; | |
}); | |
services.AddControllers(); | |
services.AddSwaggerGen(c => | |
{ | |
c.SwaggerDoc("v1", new OpenApiInfo { Title = "WebApiCustomAuthOIDC", Version = "v1" }); | |
}); | |
} | |
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline. | |
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) | |
{ | |
if (env.IsDevelopment()) | |
{ | |
app.UseDeveloperExceptionPage(); | |
app.UseSwagger(); | |
app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "WebApiCustomAuthOIDC v1")); | |
} | |
app.UseHttpsRedirection(); | |
app.UseRouting(); | |
app.UseAuthentication(); | |
app.UseAuthorization(); | |
app.UseEndpoints(endpoints => | |
{ | |
endpoints.MapControllers(); | |
}); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Implements an ASP.NET (Core) custom Authentication Handler and Scheme for OIDC tokens (user_id) passed in the Authorization HTTP header (Bearer Token; see: (https://datatracker.ietf.org/doc/html/rfc6750)) The CertificatesUrl setting in appsetting.json is for Google Identity Platform/Firebase. For Google/Cloud Identity or IAP endpoints, see: this (https://github.com/salrashid123/google_id_token#how-to-verify-an-id-token).
Special thanks to these posts for helping show the way:
Google.Apis.Auth repo can be found here: (https://github.com/googleapis/google-api-dotnet-client/tree/master/Src/Support/Google.Apis.Auth)