Skip to content

Commit

Permalink
Add authentication cookie model and configure data protection settings
Browse files Browse the repository at this point in the history
  • Loading branch information
barzin144 committed Jan 9, 2025
1 parent 6ddd17c commit d91338f
Show file tree
Hide file tree
Showing 11 changed files with 278 additions and 232 deletions.
17 changes: 17 additions & 0 deletions .env_SAMPLE
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
ConnectionStrings__MongoDb= "mongodb://USERNAME:PASSWORD@mongo:27017"
Jwt__PrivateKey= "PRIVATE_KEY"
Jwt__Issuer= "https://localhost:8001"
Jwt__AccessTokenExpirationMinutes= 2
Jwt__Audience= "http://localhost:5010"
Jwt__DataProtectionApplicationName= "microidp"
Jwt__DataProtectionKeysPath= "./DataProtectionKeys"
Jwt__CookieName= "microidp"
Jwt__DataProtectionPurpose= "JwtCookieEncryption"
OAuth__GoogleCallbackURL= "https://localhost:8001/api/auth/google-callback"
OAuth__GoogleClientId= "GOOGLE_CLIENT_ID"
OAuth__GoogleClientSecret= "GOOGLE_CLIENT_SECRET"
ASPNETCORE_URLS= "https://+;http://+"
ASPNETCORE_Kestrel__Certificates__Default__Path= "/https/aspnetcore.pfx"
ASPNETCORE_Kestrel__Certificates__Default__Password= "1234567890"
Cors__Origins= "http://localhost:3000"
DBName= "microIDP"
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,4 @@ aspnetcore.crt
aspnetcore.key
aspnetcore.pfx
**/DataProtectionKeys/
.env
7 changes: 7 additions & 0 deletions Domain/Models/AuthCookie.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Domain.Models;

public class AuthCookie
{
public string AccessToken { get; set; }
public string RefreshToken { get; set; }
}
3 changes: 3 additions & 0 deletions Domain/Models/JwtOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,8 @@ public class JwtOptions
public bool AllowSignoutAllUserActiveClients { set; get; }
public string DataProtectionApplicationName { get; set; }
public string DataProtectionKeysPath { get; set; }
public string DataProtectionPurpose { get; set; }
public string CookieName { get; set; }

}
}
47 changes: 35 additions & 12 deletions IoCConfig/ConfigureServicesExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.OpenApi.Models;
using System.Collections.Generic;
using DataAccess;
using MongoDB.Driver;
using Domain.Repositories;
Expand All @@ -14,6 +13,10 @@
using System.Security.Cryptography;
using System;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.DataProtection;
using System.IO;
using System.Threading.Tasks;
using System.Text.Json;

namespace IoCConfig
{
Expand Down Expand Up @@ -59,9 +62,40 @@ public static void AddCustomAuthentication(this IServiceCollection services, ICo
ValidAudience = configuration["Jwt:Audience"],
IssuerSigningKey = new RsaSecurityKey(rsa)
};

options.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
if (context.Request.Cookies.TryGetValue(configuration["Jwt:CookieName"], out var encryptedToken))
{
var dataProtector = context.HttpContext.RequestServices
.GetRequiredService<IDataProtectionProvider>()
.CreateProtector(configuration["Jwt:DataProtectionPurpose"]);

try
{
var authCookie = JsonSerializer.Deserialize<AuthCookie>(dataProtector.Unprotect(encryptedToken));
context.Token = authCookie.AccessToken;
}
catch
{
context.Fail("Invalid or tampered token");
}
}

return Task.CompletedTask;
}
};
});
}

public static void AddCustomDataProtection(this IServiceCollection services, IConfiguration configuration)
{
services.AddDataProtection()
.PersistKeysToFileSystem(new DirectoryInfo(configuration["Jwt:DataProtectionKeysPath"]))
.SetApplicationName(configuration["Jwt:DataProtectionApplicationName"]);
}
public static void AddCustomServices(this IServiceCollection services)
{
services.AddScoped<IJwtTokenService, JwtTokenService>();
Expand All @@ -87,17 +121,6 @@ public static void AddCustomSwagger(this IServiceCollection services)
Title = "Micro IDP API Document",
Version = "v1"
});

options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
{
Description = @"JWT Authorization header using the Bearer scheme.",
Name = "Authorization",
In = ParameterLocation.Header,
Type = SecuritySchemeType.ApiKey,
Scheme = "Bearer"
});

options.AddSecurityRequirement(new OpenApiSecurityRequirement() { { new OpenApiSecurityScheme { Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "Bearer" }, Scheme = "oauth2", Name = "Bearer", In = ParameterLocation.Header, }, new List<string>() } });
});
}

Expand Down
255 changes: 138 additions & 117 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,140 +1,161 @@
# Micro IDP Service

## Features:
## Features

- Sign up with Email
- Sign in with Email
- Sign in with Google
- Generate JWT
- Generate Refresh Token
- Sign up/in with Email
- Sign up/in with Google

## Usage
## Run in Docker

- #### Generate Private and Public key
### Generate certificate to host application with Docker over HTTPS

- C# Interactive
#### Windows

```C#
using System.Security.Cryptography;
using (var rsa = RSA.Create(2048))
{
// Export the private key
var privateKey = rsa.ExportRSAPrivateKey();
var privateKeyBase64 = Convert.ToBase64String(privateKey);
Console.WriteLine("Private Key:");
Console.WriteLine(privateKeyBase64);
```shell
dotnet dev-certs https -ep %USERPROFILE%\.aspnet\https\aspnetapp.pfx -p <CREDENTIAL_PLACEHOLDER>
dotnet dev-certs https --trust
```

// Export the public key
var publicKey = rsa.ExportRSAPublicKey();
var publicKeyBase64 = Convert.ToBase64String(publicKey);
Console.WriteLine("\nPublic Key:");
Console.WriteLine(publicKeyBase64);
}
```
In the preceding commands, replace `<CREDENTIAL_PLACEHOLDER>` with a password.

- BASH
#### Linux

```SHELL
openssl genpkey -algorithm RSA -out private_key.pem -pkeyopt rsa_keygen_bits:2048
cat private_key.pem | base64
```shell
openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout aspnetcore.key -out aspnetcore.crt -subj "/CN=localhost"
openssl pkcs12 -export -out aspnetcore.pfx -inkey aspnetcore.key -in aspnetcore.crt
```

openssl rsa -pubout -in private_key.pem -out public_key.pem
cat public_key.pem | base64
```
Replace **_ASPNETCORE_Kestrel\_\_Certificates\_\_Default\_\_Password_** with the certificate password in `.env`.

- #### Replace PRIVATE_KEY placeholder in docker-compose.yml with generated private key
Replace the volume mount source with the generated certificate path in `docker-compose.yml`:

```YML
webapi:
build: .
ports:
- 8000:80
- 8001:443
environment:
JWT__PrivateKey: "PRIVATE_KEY"
```
```yml
volumes:
- type: bind
source: ./aspnetcore.pfx
target: /https/aspnetcore.pfx
```
- #### Generate certificate to host application with docker over HTTPS
To share data protection keys for encrypting and decrypting cookies, create an empty folder and bind it into the Docker container:
- windows
```yml
volumes:
- type: bind
source: ./DataProtectionKeys
target: /app/DataProtectionKeys
```
```SHELL
dotnet dev-certs https -ep %USERPROFILE%\.aspnet\https\aspnetapp.pfx -p <CREDENTIAL_PLACEHOLDER>
dotnet dev-certs https --trust
```
### Generate Private and Public Key
In the preceding commands, replace <CREDENTIAL_PLACEHOLDER> with a password.
#### C# Interactive
- Linux
```csharp
using System.Security.Cryptography;
using (var rsa = RSA.Create(2048))
{
// Export the private key
var privateKey = rsa.ExportRSAPrivateKey();
var privateKeyBase64 = Convert.ToBase64String(privateKey);
Console.WriteLine("Private Key:");
Console.WriteLine(privateKeyBase64);

```SHELL
openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout aspnetcore.key -out aspnetcore.crt -subj "/CN=localhost"
```
// Export the public key
var publicKey = rsa.ExportRSAPublicKey();
var publicKeyBase64 = Convert.ToBase64String(publicKey);
Console.WriteLine("\nPublic Key:");
Console.WriteLine(publicKeyBase64);
}
```

```SHELL
openssl pkcs12 -export -out aspnetcore.pfx -inkey aspnetcore.key -in aspnetcore.crt
```
#### Bash

Replace **_ASPNETCORE_Kestrel\_\_Certificates\_\_Default\_\_Password_** with certificate password in docker-compose.yml
```shell
openssl genpkey -algorithm RSA -out private_key.pem -pkeyopt rsa_keygen_bits:2048
cat private_key.pem | base64

Replace volume mount source with generated certificate path in docker-compose.yml
openssl rsa -pubout -in private_key.pem -out public_key.pem
cat public_key.pem | base64
```

```YML
volumes:
- type: bind
source: ./aspnetcore.pfx
target: /https/aspnetcore.pfx
```

- #### Sing in with Google configuration

- Create OAuth2.0 client in [Google Cloud Console](https://console.cloud.google.com).
- Replace **_OAuth_GoogleClientId_** placeholder in docker-compose.yml
- Replace **_OAuth_GoogleClientSecret_** placeholder in docker-compose.yml
- Replace **_OAuth_GoogleCallBackURL_** placeholder in docker-compose.yml with your client app google callback page (this page should call https://IDP_SERVER_URL/api/auth/google-callback to get JWT)

- #### Run IDP

```SHELL
docker compose up --wait
```

### Client App

- #### Add Jwt section to you appsettings.json

```JSON
"Jwt": {
"PublicKey": "PUBLIC_KEY",
"Issuer": "https://localhost:8001",
"Audience": "http://localhost:5010"
}
```

Replace PUBLIC_KEY placeholder with generated public key

- #### Install JwtBearer package
```SHELL
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
```
- #### Add Authentication middleware

```C#
var rsa = RSA.Create();
rsa.ImportRSAPublicKey(Convert.FromBase64String(configuration["Jwt:PublicKey"] ?? ""), out _);
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = configuration["Jwt:Issuer"],
ValidAudience = configuration["Jwt:Audience"],
IssuerSigningKey = new RsaSecurityKey(rsa)
};
});
```
Replace the `PRIVATE_KEY` placeholder in `.env` with the generated private key.

### Sign in with Google Configuration

1. Create an OAuth 2.0 client in [Google Cloud Console](https://console.cloud.google.com).
2. Replace **_OAuth\_\_GoogleClientId_** placeholder in `.env`.
3. Replace **_OAuth\_\_GoogleClientSecret_** placeholder in `.env`.
4. Replace **_OAuth\_\_GoogleCallBackURL_** placeholder in `.env` with your client app's Google callback page (this page should call `https://IDP_SERVER_URL/api/auth/google-callback` to get JWT).

### Run IDP

```shell
docker compose up --wait
```

## Client App

### Add Jwt Section to Your `appsettings.json`

```json
"Jwt": {
"PublicKey": "PUBLIC_KEY",
"Issuer": "https://localhost:8001",
"Audience": "http://localhost:5010",
"CookieName": "SAME AS IDP .env Jwt__CookieName",
"DataProtectionPurpose": "SAME AS IDP .env Jwt__DataProtectionPurpose"
}
```

Replace the `PUBLIC_KEY` placeholder with the generated public key.

### Install JwtBearer Package

```shell
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
```

### Add Authentication Middleware

```csharp
var rsa = RSA.Create();
rsa.ImportRSAPublicKey(Convert.FromBase64String(configuration["Jwt:PublicKey"] ?? ""), out _);

services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = configuration["Jwt:Issuer"],
ValidAudience = configuration["Jwt:Audience"],
IssuerSigningKey = new RsaSecurityKey(rsa)
};
options.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
if (context.Request.Cookies.TryGetValue(configuration["Jwt:CookieName"], out var encryptedToken))
{
var dataProtector = context.HttpContext.RequestServices
.GetRequiredService<IDataProtectionProvider>()
.CreateProtector(configuration["Jwt:DataProtectionPurpose"]);

try
{
var authCookie = JsonSerializer.Deserialize<AuthCookie>(dataProtector.Unprotect(encryptedToken));
context.Token = authCookie.AccessToken;
}
catch
{
context.Fail("Invalid or tampered token");
}
}

return Task.CompletedTask;
}
};
});
```
Loading

0 comments on commit d91338f

Please sign in to comment.