Created
January 15, 2023 15:24
-
-
Save OnurGumus/18b1751df68a0763af4102f7ae2be1ae to your computer and use it in GitHub Desktop.
private JWT auth
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
module GiraffeJWTAuthExample.App | |
open System | |
open System.IO | |
open Microsoft.AspNetCore.Builder | |
open Microsoft.AspNetCore.Cors.Infrastructure | |
open Microsoft.AspNetCore.Hosting | |
open Microsoft.AspNetCore.Http | |
open Microsoft.Extensions.Hosting | |
open Microsoft.Extensions.Logging | |
open Microsoft.Extensions.DependencyInjection | |
open Giraffe | |
open Microsoft.AspNetCore.Authentication.JwtBearer | |
open Microsoft.AspNetCore.Authorization | |
open Microsoft.IdentityModel.Tokens // for SymmetricSecurityKey | |
open System.Text // for Encoding | |
open System.Security.Claims // For Claim, ClaimTypes | |
open System.Security.Cryptography | |
open System.IdentityModel.Tokens.Jwt | |
// --------------------------------- | |
// Web app | |
// --------------------------------- | |
let authenticate = | |
requiresAuthentication (challenge JwtBearerDefaults.AuthenticationScheme >=> text "please authenticate") | |
let requireAdminRole : HttpHandler = | |
requiresRole "Admin" (RequestErrors.FORBIDDEN "Permission denied. You must be an admin.") | |
let requireUserRole : HttpHandler = | |
requiresRoleOf ["Admin" ; "User"] (RequestErrors.FORBIDDEN "Permission denied. You must be an admin or user.") | |
let requireAdminPolicy : HttpHandler = | |
authorizeByPolicyName "AdminPolicy" (RequestErrors.FORBIDDEN "Permission denied. Failed Admin Policy") | |
let requireMustBe21ByPolicy : HttpHandler = | |
authorizeByPolicyName "MustBe21" (RequestErrors.FORBIDDEN "Permission denied. Not old enough!") | |
let userMinimumAge (minAge : int) (user : ClaimsPrincipal) = | |
match user.Claims |> Seq.tryFind (fun c -> c.Type = ClaimTypes.DateOfBirth) with | |
| Some dobClaim -> | |
let dob = Convert.ToDateTime(dobClaim.Value) | |
let age = DateTime.Today.Year - dob.Year | |
// account for leap year https://stackoverflow.com/a/1404/ | |
let correctedAge = if dob > DateTime.Today.AddYears(age) then age - 1 else age | |
correctedAge >= minAge | |
| None -> | |
false | |
let requireMustBe21Manually : HttpHandler = | |
fun next ctx -> | |
if userMinimumAge 21 ctx.User then | |
next ctx | |
else | |
RequestErrors.FORBIDDEN "You have to be at least 21" next ctx | |
let webApp = | |
choose [ | |
GET >=> | |
choose [ | |
route "/" >=> text "unprotected content accessible by anyone" | |
authenticate >=> choose [ | |
route "/admin" >=> requireAdminRole >=> text "welcome, admin" | |
route "/user" >=> requireUserRole >=> text "welcome, user" | |
route "/tavern-claims" >=> requireMustBe21Manually >=> text "welcome, here's your gin and tonic!" | |
route "/admin-policy" >=> requireAdminPolicy >=> text "welcome, admin (by policy)" | |
route "/tavern-policy" >=> requireMustBe21ByPolicy >=> text "welcome, here's your gin and tonic and policy!" | |
] | |
] | |
setStatusCode 404 >=> text "Not Found" ] | |
// --------------------------------- | |
// Error handler | |
// --------------------------------- | |
let errorHandler (ex : Exception) (logger : ILogger) = | |
logger.LogError(ex, "An unhandled exception has occurred while executing the request.") | |
clearResponse >=> setStatusCode 500 >=> text ex.Message | |
// --------------------------------- | |
// Config and Main | |
// --------------------------------- | |
let configureCors (builder : CorsPolicyBuilder) = | |
builder | |
.WithOrigins( | |
"http://localhost:5000", | |
"https://localhost:5001") | |
.AllowAnyMethod() | |
.AllowAnyHeader() | |
|> ignore | |
let configureApp (app : IApplicationBuilder) = | |
let env = app.ApplicationServices.GetService<IWebHostEnvironment>() | |
(match env.IsDevelopment() with | |
| true -> | |
app.UseDeveloperExceptionPage() | |
| false -> | |
app .UseGiraffeErrorHandler(errorHandler) | |
.UseHttpsRedirection()) | |
.UseCors(configureCors) | |
.UseStaticFiles() | |
.UseAuthentication() // Before UseGiraffe | |
.UseGiraffe(webApp) | |
let key = "pleaseReplaceWithYourSecretKeyRetrievedFromSomeSecureLocation" | |
let issuer = "https://certificateissuer.example.com/" | |
let audience = "https://backendserver.example.com" | |
let now = DateTime.UtcNow; | |
let claims = [| | |
new Claim(JwtRegisteredClaimNames.Sub, "YOUR_CLIENTID") | |
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()) | |
Claim(ClaimTypes.Role, "User") | |
Claim(ClaimTypes.DateOfBirth, "1980-01-01") | |
|] | |
let handler = new JwtSecurityTokenHandler(); | |
let rsa = RSA.Create(); | |
if(File.Exists("privatekey.pem")) then | |
let pkey = File.ReadAllBytes("privatekey.pem") | |
rsa.ImportRSAPrivateKey(pkey) |> ignore | |
else | |
let pkey = rsa.ExportRSAPrivateKey() | |
File.WriteAllBytes("privatekey.pem", pkey) | |
let token = | |
new JwtSecurityToken | |
( | |
issuer, | |
audience, | |
claims, | |
now.AddMilliseconds(-30), | |
now.AddMinutes(60), | |
new SigningCredentials(RsaSecurityKey(rsa), SecurityAlgorithms.RsaSha256) | |
); | |
// handler.WriteToken(token) returns the token ready to send to AaaS ! | |
let myJWT = handler.WriteToken(token) | |
let configureServices (services : IServiceCollection) = | |
services.AddAuthentication(fun opt -> | |
opt.DefaultAuthenticateScheme <- JwtBearerDefaults.AuthenticationScheme | |
).AddJwtBearer(fun (opt : JwtBearerOptions )-> | |
// https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.authentication.jwtbearer.jwtbeareroptions?view=aspnetcore-5.0 | |
opt.TokenValidationParameters <- TokenValidationParameters( | |
// https://docs.microsoft.com/en-us/dotnet/api/microsoft.identitymodel.tokens.tokenvalidationparameters?view=azure-dotnet | |
ValidateIssuerSigningKey = true, | |
IssuerSigningKey = new RsaSecurityKey(rsa), | |
ValidIssuer = issuer, | |
ValidateIssuer = true, | |
ValidateAudience = true, | |
ValidAudience = audience | |
)) |> ignore | |
services.AddAuthorization(fun (options : AuthorizationOptions) -> | |
options.AddPolicy( "AdminPolicy", fun policy -> | |
policy.RequireClaim(ClaimTypes.Role, "Admin") |> ignore | |
// you can require other claims as well here | |
) |> ignore | |
options.AddPolicy("MustBe21", fun policy -> | |
policy.RequireAssertion( | |
fun (context : AuthorizationHandlerContext) -> | |
userMinimumAge 21 context.User | |
) |> ignore | |
) |> ignore | |
) |> ignore | |
services.AddCors() |> ignore | |
services.AddGiraffe() |> ignore | |
let configureLogging (builder : ILoggingBuilder) = | |
builder.AddConsole() | |
.AddDebug() |> ignore | |
[<EntryPoint>] | |
let main args = | |
let contentRoot = Directory.GetCurrentDirectory() | |
let webRoot = Path.Combine(contentRoot, "WebRoot") | |
Host.CreateDefaultBuilder(args) | |
.ConfigureWebHostDefaults( | |
fun webHostBuilder -> | |
webHostBuilder | |
.UseContentRoot(contentRoot) | |
.UseWebRoot(webRoot) | |
.Configure(Action<IApplicationBuilder> configureApp) | |
.ConfigureServices(configureServices) | |
.ConfigureLogging(configureLogging) | |
|> ignore) | |
.Build() | |
.Run() | |
0 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment