Skip to content

Commit

Permalink
add keycloak provider
Browse files Browse the repository at this point in the history
  • Loading branch information
pilcrowOnPaper committed Jul 3, 2024
1 parent 7e1bcea commit 81838ca
Show file tree
Hide file tree
Showing 10 changed files with 185 additions and 6 deletions.
1 change: 1 addition & 0 deletions .changesets/1719992870-v9x3a.minor.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add KeyCloak provider
3 changes: 1 addition & 2 deletions .github/workflows/publish.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ jobs:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v1

publish-v2-docs:
name: Publish v2 docs
runs-on: ubuntu-latest
Expand All @@ -102,4 +102,3 @@ jobs:
run: npm i -g wrangler
- name: deploy
run: wrangler pages deploy docs/dist --project-name arctic-v2 --branch main

1 change: 1 addition & 0 deletions docs/malta.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
["Google", "/providers/google"],
["Intuit", "/providers/intuit"],
["Kakao", "/providers/kakao"],
["KeyCloak", "/providers/keycloak"],
["Lichess", "/providers/lichess"],
["Line", "/providers/line"],
["Linear", "/providers/linear"],
Expand Down
1 change: 0 additions & 1 deletion docs/pages/providers/authentik.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,6 @@ const idToken = tokens.idToken();
const claims = decodeIdToken(idToken);
```


## Refresh access tokens

Use `refreshAccessToken()` to get a new access token using a refresh token. This method also returns `OAuth2Tokens` and throws the same errors as `validateAuthorizationCode()`.
Expand Down
115 changes: 115 additions & 0 deletions docs/pages/providers/keycloak.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
---
title: "KeyCloak"
---

# Okta

OAuth 2.0 provider for KeyCloak.

Also see the [OAuth 2.0 with PKCE](/guides/oauth2-pkce) guide.

## Initialization

The realm URL should not include trailing slashes.

```ts
import { KeyCloak } from "arctic";

const realmURL = "https://auth.example.com/realms/myrealm
const keycloak = new KeyCloak(realmURL, clientId, clientSecret, redirectURI);
```

## Create authorization URL

Use `addScopes()` to define scopes.

```ts
import { generateState, generateCodeVerifier } from "arctic";

const state = generateState();
const codeVerifier = generateCodeVerifier();
const url = keycloak.createAuthorizationURL(state, codeVerifier);
url.addScopes("openid", "profile");
```

## Validate authorization code

`validateAuthorizationCode()` will either return an [`OAuth2Tokens`](/reference/main/OAuth2Tokens), or throw one of [`OAuth2RequestError`](/reference/main/OAuth2RequestError), [`ArcticFetchError`](/reference/main/ArcticFetchError), or a standard `Error` (parse errors). Actual values returned by KeyCloak depends on your configuration and version.

```ts
import { OAuth2RequestError, ArcticFetchError } from "arctic";

try {
const tokens = await keycloak.validateAuthorizationCode(code, codeVerifier);
const accessToken = tokens.accessToken();
const accessTokenExpiresAt = tokens.accessTokenExpiresAt();
const refreshToken = tokens.refreshToken();
} catch (e) {
if (e instanceof OAuth2RequestError) {
// Invalid authorization code, credentials, or redirect URI
const code = e.code;
// ...
}
if (e instanceof ArcticFetchError) {
// Failed to call `fetch()`
const cause = e.cause;
// ...
}
// Parse error
}
```

## OpenID Connect

Use OpenID Connect with the `openid` scope to get the user's profile with an ID token or the `userinfo` endpoint. Arctic provides [`decodeIdToken()`](/reference/main/decodeIdToken) for decoding the token's payload.

```ts
const url = keycloak.createAuthorizationURL(state, codeVerifier);
url.addScopes("openid");
```

```ts
import { decodeIdToken } from "arctic";

const tokens = await keycloak.validateAuthorizationCode(code, codeVerifier);
const idToken = tokens.idToken();
const claims = decodeIdToken(idToken);
```

## Refresh access tokens

Use `refreshAccessToken()` to get a new access token using a refresh token. This method also returns `OAuth2Tokens` and throws the same errors as `validateAuthorizationCode()`.

```ts
import { OAuth2RequestError, ArcticFetchError } from "arctic";

try {
const tokens = await keycloak.refreshAccessToken(accessToken);
} catch (e) {
if (e instanceof OAuth2RequestError) {
// Invalid authorization code, credentials, or redirect URI
}
if (e instanceof ArcticFetchError) {
// Failed to call `fetch()`
}
// Parse error
}
```

## Revoke tokens

Use `revokeToken()` to revoke a token. This can throw the same errors as `validateAuthorizationCode()`.

```ts
try {
await keycloak.revokeToken(token);
} catch (e) {
if (e instanceof OAuth2RequestError) {
// Invalid authorization code, credentials, or redirect URI
}
if (e instanceof ArcticFetchError) {
// Failed to call `fetch()`
}
// Parse error
}
```
2 changes: 1 addition & 1 deletion docs/pages/reference/main/ArcticFetchError.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@ title: "ArcticFetchError"

Extends `Error`.

Error indicating that the `fetch()` call failed. See `ArcticFetchError.cause` for the error thrown by `fetch()`.
Error indicating that the `fetch()` call failed. See `ArcticFetchError.cause` for the error thrown by `fetch()`.
1 change: 0 additions & 1 deletion docs/pages/reference/main/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,3 @@ title: "arctic"
- [`generateCodeVerifier()`](https://oauth2.oslojs.dev/reference/main/generateCodeVerifier)
- [`generateState()`](https://oauth2.oslojs.dev/reference/main/generateState)
- [`decodeIdToken()`](/reference/main/decodeIdToken)

1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export { GitHub } from "./providers/github.js";
export { GitLab } from "./providers/gitlab.js";
export { Google } from "./providers/google.js";
export { Kakao } from "./providers/kakao.js";
export { KeyCloak } from "./providers/keycloak.js";
export { Lichess } from "./providers/lichess.js";
export { Line } from "./providers/line.js";
export { Linear } from "./providers/linear.js";
Expand Down
2 changes: 1 addition & 1 deletion src/providers/authentik.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,4 @@ export class Authentik {
context.authenticateWithHTTPBasicAuth(this.clientId, this.clientSecret);
await sendTokenRevocationRequest(this.tokenRevocationEndpoint, context);
}
}
}
64 changes: 64 additions & 0 deletions src/providers/keycloak.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import {
AuthorizationCodeAuthorizationURL,
AuthorizationCodeTokenRequestContext,
RefreshRequestContext,
TokenRevocationRequestContext
} from "@oslojs/oauth2";
import { sendTokenRequest, sendTokenRevocationRequest } from "../request.js";

import type { OAuth2Tokens } from "../oauth2.js";

export class KeyCloak {
private authorizationEndpoint: string;
private tokenEndpoint: string;
private tokenRevocationEndpoint: string;

private clientId: string;
private clientSecret: string;
private redirectURI: string;

constructor(realmURL: string, clientId: string, clientSecret: string, redirectURI: string) {
this.authorizationEndpoint = realmURL + "/protocol/openid-connect/auth";
this.tokenEndpoint = realmURL + "/protocol/openid-connect/token";
this.tokenRevocationEndpoint = realmURL + "/protocol/openid-connect/revoke";
this.clientId = clientId;
this.clientSecret = clientSecret;
this.redirectURI = redirectURI;
}

public createAuthorizationURL(
state: string,
codeVerifier: string
): AuthorizationCodeAuthorizationURL {
const url = new AuthorizationCodeAuthorizationURL(this.authorizationEndpoint, this.clientId);
url.setRedirectURI(this.redirectURI);
url.setState(state);
url.setS256CodeChallenge(codeVerifier);
return url;
}

public async validateAuthorizationCode(
code: string,
codeVerifier: string
): Promise<OAuth2Tokens> {
const context = new AuthorizationCodeTokenRequestContext(code);
context.authenticateWithHTTPBasicAuth(this.clientId, this.clientSecret);
context.setRedirectURI(this.redirectURI);
context.setCodeVerifier(codeVerifier);
const tokens = await sendTokenRequest(this.tokenEndpoint, context);
return tokens;
}

public async refreshAccessToken(refreshToken: string): Promise<OAuth2Tokens> {
const context = new RefreshRequestContext(refreshToken);
context.authenticateWithHTTPBasicAuth(this.clientId, this.clientSecret);
const tokens = await sendTokenRequest(this.tokenEndpoint, context);
return tokens;
}

public async revokeToken(token: string): Promise<void> {
const context = new TokenRevocationRequestContext(token);
context.authenticateWithHTTPBasicAuth(this.clientId, this.clientSecret);
await sendTokenRevocationRequest(this.tokenRevocationEndpoint, context);
}
}

0 comments on commit 81838ca

Please sign in to comment.