Skip to content

Commit

Permalink
Merge pull request #44 from malstroem-labs/41-replace-signed-jwt-bear…
Browse files Browse the repository at this point in the history
…er-with-pat

Replace signed JWT bearer with PAT
  • Loading branch information
Apollo3zehn authored Mar 7, 2024
2 parents ff016f8 + 6ec808c commit 06eaab9
Show file tree
Hide file tree
Showing 55 changed files with 2,337 additions and 2,947 deletions.
15 changes: 13 additions & 2 deletions notes/auth.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
# Note
The text below does not fully apply anymore to Nexus because we have switched from refresh tokens + access tokens to personal access tokens that expire only optionally and are not cryptographically signed but checked against the database instead. The negible problem of higher database load is acceptible to get the benefit of not having to manage refresh tokens which are prone to being revoked as soon as the user uses it in more than a single place.

The new personal access tokens approach allows fine-grained access control to catalogs and makes many parts of the code much simpler. Current status is:
- User can manage personal access tokens in the web interface and specify read or read/write access to specific catalogs.
- The token the user gets is a string which consists of a combination of the token secret (a long random base64 encoded number) and the user id.
- Tokens are stored on disk in the folder configured by the `PathsOptions.Users` option in a files named `tokens.json`. They loaded lazily into memory on first demand and kept there for future requests.
- When the token is part of the Authorization header (`Authorization: Bearer <token>`) it is being handles by the `PersonalAccessTokenAuthenticationHandler` which creates a `ClaimsPrincipal` if the token is valid.
- The claims that are associated with the token can be anything but right now only the claims `CanReadCatalog` and `CanWriteCatalog` are being considered. To avoid a token to be more powerful than the user itself, the user claims are also being checked (see `AuthUtilities.cs`) on each request.
- The lifetime of the tokens can be choosen by the users or left untouched to produce tokens with unlimited lifetime.

# Authentication and Authorization

Nexus exposes resources (data, metadata and more) via HTTP API. Most of these resources do not have specific owners - they are owned by the system itself. Most of these resources need to be protected which makes an `authorization` mechanism necessary.
Expand Down Expand Up @@ -50,7 +61,7 @@ In order to detect a compromised token, it is recommended to implement token rot
## Implementation details

The backend of Nexus is a confidential client upon user request, it will perform the authorization code flow to obtain an ID token to authenticate and sign-in the user.
The backend of Nexus is a confidential client and upon user request, it will perform the authorization code flow to obtain an ID token to authenticate and sign-in the user.

Nexus supports multiple OpenID Connect providers. See [Configuration] on how to add configuration values.

Expand All @@ -66,7 +77,7 @@ The problem now is that although the access token contains the subject claim, it

Another problem is that Nexus cannot add these user-specific claims to the access token, which means that the user database must be consulted for every single request, resulting in a high disk load.

Also, a such client would be public which means it is possible to copy the `client_id` and use them in other clients, which might be problematic when there is limited traffic allowed .
Also, a such client would be public which means it is possible to copy the `client_id` and use them in other clients, which might be problematic when there is limited traffic allowed.

The last problem with refresh tokens is that _"for public clients [they] MUST be sender-constrained or use
refresh token rotation [...]"_ [[OAuth 2.0 Security Best Current Practice](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics-19#section-2.2.2), [RFC 6749](https://datatracker.ietf.org/doc/html/rfc6749#section-4.13)].
Expand Down
205 changes: 79 additions & 126 deletions openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -1250,60 +1250,25 @@
}
}
},
"/api/v1/users/tokens/refresh": {
"post": {
"/api/v1/users/tokens/delete": {
"delete": {
"tags": [
"Users"
],
"summary": "Refreshes the JWT token.",
"operationId": "Users_RefreshToken",
"requestBody": {
"x-name": "request",
"description": "The refresh token request.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/RefreshTokenRequest"
}
}
},
"required": true,
"x-position": 1
},
"responses": {
"200": {
"description": "A new pair of JWT and refresh token.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TokenPair"
}
}
}
"summary": "Deletes a personal access token.",
"operationId": "Users_DeleteTokenByValue",
"parameters": [
{
"name": "value",
"in": "query",
"required": true,
"description": "The personal access token to delete.",
"schema": {
"type": "string"
},
"x-position": 1
}
}
}
},
"/api/v1/users/tokens/revoke": {
"post": {
"tags": [
"Users"
],
"summary": "Revokes a refresh token.",
"operationId": "Users_RevokeToken",
"requestBody": {
"x-name": "request",
"description": "The revoke token request.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/RevokeTokenRequest"
}
}
},
"required": true,
"x-position": 1
},
"responses": {
"200": {
"description": "",
Expand Down Expand Up @@ -1340,24 +1305,14 @@
}
}
},
"/api/v1/users/tokens/generate": {
"/api/v1/users/tokens/create": {
"post": {
"tags": [
"Users"
],
"summary": "Generates a refresh token.",
"operationId": "Users_GenerateRefreshToken",
"summary": "Creates a personal access token.",
"operationId": "Users_CreateToken",
"parameters": [
{
"name": "description",
"in": "query",
"required": true,
"description": "The refresh token description.",
"schema": {
"type": "string"
},
"x-position": 1
},
{
"name": "userId",
"in": "query",
Expand All @@ -1369,6 +1324,19 @@
"x-position": 2
}
],
"requestBody": {
"x-name": "token",
"description": "The personal access token to create.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PersonalAccessToken"
}
}
},
"required": true,
"x-position": 1
},
"responses": {
"200": {
"description": "",
Expand All @@ -1383,21 +1351,22 @@
}
}
},
"/api/v1/users/accept-license": {
"get": {
"/api/v1/users/tokens/{tokenId}": {
"delete": {
"tags": [
"Users"
],
"summary": "Accepts the license of the specified catalog.",
"operationId": "Users_AcceptLicense",
"summary": "Deletes a personal access token.",
"operationId": "Users_DeleteToken",
"parameters": [
{
"name": "catalogId",
"in": "query",
"name": "tokenId",
"in": "path",
"required": true,
"description": "The catalog identifier.",
"description": "The identifier of the personal access token.",
"schema": {
"type": "string"
"type": "string",
"format": "guid"
},
"x-position": 1
}
Expand All @@ -1417,22 +1386,21 @@
}
}
},
"/api/v1/users/tokens/{tokenId}": {
"delete": {
"/api/v1/users/accept-license": {
"get": {
"tags": [
"Users"
],
"summary": "Deletes a refresh token.",
"operationId": "Users_DeleteRefreshToken",
"summary": "Accepts the license of the specified catalog.",
"operationId": "Users_AcceptLicense",
"parameters": [
{
"name": "tokenId",
"in": "path",
"name": "catalogId",
"in": "query",
"required": true,
"description": "The identifier of the refresh token.",
"description": "The catalog identifier.",
"schema": {
"type": "string",
"format": "guid"
"type": "string"
},
"x-position": 1
}
Expand Down Expand Up @@ -1663,8 +1631,8 @@
"tags": [
"Users"
],
"summary": "Gets all refresh tokens.",
"operationId": "Users_GetRefreshTokens",
"summary": "Gets all personal access tokens.",
"operationId": "Users_GetTokens",
"parameters": [
{
"name": "userId",
Expand All @@ -1685,7 +1653,7 @@
"schema": {
"type": "object",
"additionalProperties": {
"$ref": "#/components/schemas/RefreshToken"
"$ref": "#/components/schemas/PersonalAccessToken"
}
}
}
Expand Down Expand Up @@ -2224,43 +2192,6 @@
}
}
},
"TokenPair": {
"type": "object",
"description": "A token pair.",
"additionalProperties": false,
"properties": {
"accessToken": {
"type": "string",
"description": "The JWT token."
},
"refreshToken": {
"type": "string",
"description": "The refresh token."
}
}
},
"RefreshTokenRequest": {
"type": "object",
"description": "A refresh token request.",
"additionalProperties": false,
"properties": {
"refreshToken": {
"type": "string",
"description": "The refresh token."
}
}
},
"RevokeTokenRequest": {
"type": "object",
"description": "A revoke token request.",
"additionalProperties": false,
"properties": {
"refreshToken": {
"type": "string",
"description": "The refresh token."
}
}
},
"MeResponse": {
"type": "object",
"description": "A me response.",
Expand All @@ -2282,11 +2213,11 @@
"type": "boolean",
"description": "A boolean which indicates if the user is an administrator."
},
"refreshTokens": {
"personalAccessTokens": {
"type": "object",
"description": "A list of currently present refresh tokens.",
"description": "A list of personal access tokens.",
"additionalProperties": {
"$ref": "#/components/schemas/RefreshToken"
"$ref": "#/components/schemas/PersonalAccessToken"
}
}
}
Expand All @@ -2302,19 +2233,41 @@
}
}
},
"RefreshToken": {
"PersonalAccessToken": {
"type": "object",
"description": "A refresh token.",
"description": "A personal access token.",
"additionalProperties": false,
"properties": {
"description": {
"type": "string",
"description": "The token description."
},
"expires": {
"type": "string",
"description": "The date/time when the token expires.",
"format": "date-time"
},
"description": {
"claims": {
"type": "array",
"description": "The claims that will be part of the token.",
"items": {
"$ref": "#/components/schemas/TokenClaim"
}
}
}
},
"TokenClaim": {
"type": "object",
"description": "A revoke token request.",
"additionalProperties": false,
"properties": {
"type": {
"type": "string",
"description": "The token description."
"description": "The claim type."
},
"value": {
"type": "string",
"description": "The claim value."
}
}
},
Expand Down
Loading

0 comments on commit 06eaab9

Please sign in to comment.