Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Authentication in Swagger UI & documentation #270

Merged
merged 5 commits into from
Sep 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
- Added grid to manage organisations in administration area.
- Added grid to manage users in administration area.
- Added local Keycloak server for development.
- Added authentication in Swagger UI.

### Changed

Expand Down
45 changes: 45 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,48 @@ docker inspect --format='{{json .State.Health.Status}}' container_name
## Neue Version erstellen

Ein neuer GitHub _Pre-release_ wird bei jeder Änderung auf [main](https://github.com/GeoWerkstatt/geopilot) [automatisch](./.github/workflows/pre-release.yml) erstellt. In diesem Kontext wird auch ein neues Docker Image mit dem Tag _:edge_ erstellt und in die [GitHub Container Registry (ghcr.io)](https://github.com/geowerkstatt/geopilot/pkgs/container/geopilot) gepusht. Der definitve Release erfolgt, indem die Checkbox _Set as the latest release_ eines beliebigen Pre-releases gesetzt wird. In der Folge wird das entsprechende Docker Image in der ghcr.io Registry mit den Tags (bspw.: _:v1.2.3_ und _:latest_) [ergänzt](./.github/workflows/release.yml).

## Authentifizierung

Fürs Login auf geopilot wird ein Identity Provider mit OpenID Connect (OIDC) vorausgesetzt.
Der verwendete OAuth2 Flow ist _Authorization Code Flow with Proof Key for Code Exchange (PKCE)_.

### Token

Zur Authentifizierung aus dem Frontend wird das ID-Token und aus dem Swagger UI das Access-Token verwendet.
Dabei wird geprüft, dass das Token von der angegebenen Authority ausgestellt wurde (`iss` Claim) und für die Client-Id gültig ist (`aud` Claim).
Zusätzlich werden folgende Claims im Token vorausgesetzt: `sub`, `email` und `name`.
Diese werden beispielsweise bei den [OIDC Scopes](https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims) `openid`, `profile` und `email` mitgeliefert.

### Redirect URIs

Als erlaubte Redirect URIs müssen für das Login aus dem Frontend `https://<app-domain>` und aus Swagger UI `https://<app-domain>/swagger/oauth2-redirect.html` angegeben werden.
_([Entwicklungsumgebung](./config/realms/keycloak-geopilot.json): `https://localhost:5173` und `https://localhost:7188/swagger/oauth2-redirect.html`)_

### Swagger UI

Abhängig vom Identity Provider wird die Audience (`aud` Claim) im Access-Token automatisch gesetzt, sofern ein passender Scope verwendet wird.
Der benötigte Scope kann in den Appsettings under `ApiScope` gesetzt werden, um diesen im Swagger UI zur Auswahl anzuzeigen.
Ohne diesem Scope wird das Access-Token möglicherweise ohne oder für eine andere Audience ausgestellt.

In der [Entwicklungsumgebung](./config/realms/keycloak-geopilot.json) wird die Audience stattdessen mit einem Keycloak Protocol Mapper festgelegt.

### Appsettings

Folgende Appsettings können definiert werden (Beispiel aus [appsettings.Development.json](./src/Geopilot.Api/appsettings.Development.json) für die Entwicklungsumgebung):

```json5
"Auth": {
// General auth options
"Authority": "http://localhost:4011/realms/geopilot", // Token issuer (required)
"ClientId": "geopilot-client", // Token audience (required)

// Swagger UI auth options
"ApiOrigin": "https://localhost:7188", // Swagger UI origin (required)
"AuthorizationUrl": "http://localhost:4011/realms/geopilot/protocol/openid-connect/auth", // OAuth2 login URL
"TokenUrl": "http://localhost:4011/realms/geopilot/protocol/openid-connect/token", // OAuth2 token URL
"ApiScope": "<custom app scope>"
}
```

Falls die `AuthorizationUrl` und/oder `TokenUrl` nicht definiert sind, wird im Swagger UI die OpenID Konfiguration der Authority (`<authority-url>/.well-known/openid-configuration`) geladen und alle vom Identity Provider unterstützten Flows angezeigt.
22 changes: 20 additions & 2 deletions config/realms/keycloak-geopilot.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,29 @@
"alwaysDisplayInConsole": true,
"redirectUris": [
"https://localhost:5173",
"http://localhost:5173"
"http://localhost:5173",
"https://localhost:7188/swagger/oauth2-redirect.html",
"http://localhost:5173/swagger/oauth2-redirect.html"
],
"webOrigins": [
"https://localhost:5173",
"http://localhost:5173"
"http://localhost:5173",
"https://localhost:7188"
],
"protocolMappers": [
{
"name": "geopilot-audience-mapper",
"protocol": "openid-connect",
"protocolMapper": "oidc-audience-mapper",
"consentRequired": false,
"config": {
"included.client.audience": "geopilot-client",
"id.token.claim": "false",
"lightweight.claim": "false",
"access.token.claim": "true",
"introspection.token.claim": "true"
}
}
]
}
],
Expand Down
3 changes: 3 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ services:
ReverseProxy__Clusters__stacBrowserCluster__Destinations__stacBrowserDestination__Address: http://stac-browser:8080/
Auth__Authority: http://localhost:4011/realms/geopilot
Auth__ClientId: geopilot-client
Auth__AuthorizationUrl: http://localhost:4011/realms/geopilot/protocol/openid-connect/auth
Auth__TokenUrl: http://localhost:4011/realms/geopilot/protocol/openid-connect/token
Auth__ApiOrigin: http://localhost:5173
Validation__InterlisCheckServiceUrl: http://interlis-check-service:8080/
volumes:
- ./src/Geopilot.Api/Uploads:/uploads
Expand Down
17 changes: 17 additions & 0 deletions src/Geopilot.Api/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,19 @@

// Workaround for STAC API having multiple actions mapped to the "search" route.
options.ResolveConflictingActions(apiDescriptions => apiDescriptions.First());

var authUrl = builder.Configuration["Auth:AuthorizationUrl"];
var tokenUrl = builder.Configuration["Auth:TokenUrl"];
if (!string.IsNullOrEmpty(authUrl) && !string.IsNullOrEmpty(tokenUrl))
{
var apiScope = builder.Configuration["Auth:ApiScope"];
options.AddGeopilotOAuth2(authUrl, tokenUrl, apiScope);
}
else
{
var authority = builder.Configuration["Auth:Authority"];
options.AddOpenIdConnect(authority!);
}
});

builder.Services
Expand Down Expand Up @@ -189,6 +202,10 @@
app.UseSwaggerUI(options =>
{
options.SwaggerEndpoint("/swagger/v1/swagger.json", "geopilot API v1.0");

options.OAuthClientId(builder.Configuration["Auth:ClientId"]);
options.OAuth2RedirectUrl($"{builder.Configuration["Auth:ApiOrigin"]}/swagger/oauth2-redirect.html");
options.OAuthUsePkce();
});

app.UseHttpsRedirection();
Expand Down
96 changes: 96 additions & 0 deletions src/Geopilot.Api/SwaggerExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;

namespace Geopilot.Api;

/// <summary>
/// Provides extension methods related to Swagger.
/// </summary>
public static class SwaggerExtensions
{
private const string SchemeName = "Authorization";

/// <summary>
/// Adds a security definition and requirement for OpenId Connect using the well-known configuration of the <paramref name="authority"/>.
/// </summary>
/// <param name="options">The swagger options.</param>
/// <param name="authority">The authority and token issuer.</param>
public static void AddOpenIdConnect(this SwaggerGenOptions options, string authority)
{
options.AddSecurityDefinition(JwtBearerDefaults.AuthenticationScheme, new OpenApiSecurityScheme
{
Name = SchemeName,
Scheme = JwtBearerDefaults.AuthenticationScheme,
In = ParameterLocation.Header,
Type = SecuritySchemeType.OpenIdConnect,
OpenIdConnectUrl = new Uri($"{authority}/.well-known/openid-configuration"),
});
options.AddOAuth2SecurityRequirement();
}

/// <summary>
/// Adds a security definition and requirement for OAuth2 authorization code flow.
/// </summary>
/// <param name="options">The swagger options.</param>
/// <param name="authUrl">The authorization URL.</param>
/// <param name="tokenUrl">The token URL.</param>
/// <param name="apiScope">An optional scope defined for the client.</param>
public static void AddGeopilotOAuth2(this SwaggerGenOptions options, string authUrl, string tokenUrl, string? apiScope)
{
var scopes = new Dictionary<string, string>
{
{ "openid", "Open Id" },
{ "email", "User Email" },
{ "profile", "User Profile" },
};
if (apiScope != null)
{
scopes.Add(apiScope, "geopilot API (required)");
}

options.AddSecurityDefinition(JwtBearerDefaults.AuthenticationScheme, new OpenApiSecurityScheme
{
Name = SchemeName,
Scheme = JwtBearerDefaults.AuthenticationScheme,
In = ParameterLocation.Header,
Type = SecuritySchemeType.OAuth2,
Flows = new OpenApiOAuthFlows
{
AuthorizationCode = new OpenApiOAuthFlow
{
Scopes = scopes,
AuthorizationUrl = new Uri(authUrl),
TokenUrl = new Uri(tokenUrl),
RefreshUrl = new Uri(tokenUrl),
},
},
});
options.AddOAuth2SecurityRequirement();
}

/// <summary>
/// Adds a security requirement for OAuth2.
/// </summary>
/// <param name="options">The swagger options.</param>
private static void AddOAuth2SecurityRequirement(this SwaggerGenOptions options)
{
options.AddSecurityRequirement(new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = JwtBearerDefaults.AuthenticationScheme,
},
Scheme = "oauth2",
Name = JwtBearerDefaults.AuthenticationScheme,
In = ParameterLocation.Header,
},
Array.Empty<string>()
},
});
}
}
5 changes: 4 additions & 1 deletion src/Geopilot.Api/appsettings.Development.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
{
"Auth": {
"Authority": "http://localhost:4011/realms/geopilot",
"ClientId": "geopilot-client"
"ClientId": "geopilot-client",
"AuthorizationUrl": "http://localhost:4011/realms/geopilot/protocol/openid-connect/auth",
"TokenUrl": "http://localhost:4011/realms/geopilot/protocol/openid-connect/token",
"ApiOrigin": "https://localhost:7188"
},
"Logging": {
"LogLevel": {
Expand Down