-
-
Notifications
You must be signed in to change notification settings - Fork 77
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
feat: add zitadel provider #201
Open
cortopy
wants to merge
1
commit into
pilcrowonpaper:main
Choose a base branch
from
cortopy:zitadel-provider
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,144 @@ | ||
--- | ||
title: "Zitadel" | ||
--- | ||
|
||
# Zitadel | ||
|
||
OAuth 2.0 provider for Zitadel. | ||
|
||
Also see the [Authenticate users with OpenID Connect](https://zitadel.com/docs/guides/integrate/login/oidc) (OIDC) guide. | ||
|
||
## Initialization | ||
|
||
After creating a project and a client, get the domain of your instance (if using Zitadel cloud) or your self-hosted deployment. | ||
|
||
The variable should include protocol (`http`, `https`) just in case you're using something like a local docker-compose instance. Please don't use `http` in production. | ||
|
||
The Zitadel provider is designed to be used in a server-side environment, with a configuration of a client that has Authentication as None. This is the recommended configuration for Zitadel web applications and backends. As a result, no secret client is required for the flows supported by the provider. | ||
|
||
```ts | ||
import { Zitadel } from "arctic"; | ||
|
||
const domain = "https://xxxxxx.us1.zitadel.cloud"; | ||
const zitadel = new Zitadel(domain, clientId, redirectURI); | ||
``` | ||
|
||
## Create authorization URL | ||
|
||
```ts | ||
import { generateState } from "arctic"; | ||
|
||
const state = arctic.generateState(); | ||
const codeVerifier = arctic.generateCodeVerifier(); | ||
const scopes = [ | ||
// if you want the profile in the id token | ||
"openid", | ||
"profile", | ||
"email", | ||
// Required to get refresh token back in the response | ||
"offline_access", | ||
// required if you're going to use any other API endpoint | ||
// for example: /auth/v1/users/me to get full user's profile | ||
"urn:zitadel:iam:org:project:id:zitadel:aud", | ||
]; | ||
const authorizationUrl = zitadel.createAuthorizationURL( | ||
state, | ||
codeVerifier, | ||
scopes, | ||
); | ||
``` | ||
|
||
## 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). Zitadel returns an access token, the access token expiration, and a refresh token if you specified the `offline_access` scope. | ||
|
||
```ts | ||
import { OAuth2RequestError, ArcticFetchError } from "arctic"; | ||
|
||
try { | ||
const tokens = await zitadel.validateAuthorizationCode(code); | ||
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 | ||
} | ||
``` | ||
|
||
## Refresh access tokens | ||
|
||
Use `refreshAccessToken()` to get a new access token using a refresh token. Zitadel returns the same values as during the authorization code validation. This method also returns `OAuth2Tokens` and throws the same errors as `validateAuthorizationCode()` | ||
|
||
```ts | ||
import { OAuth2RequestError, ArcticFetchError } from "arctic"; | ||
|
||
try { | ||
const tokens = await zitadel.refreshAccessToken(refreshToken); | ||
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 | ||
} | ||
if (e instanceof ArcticFetchError) { | ||
// Failed to call `fetch()` | ||
} | ||
// 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 scopes = ["openid"]; | ||
const url = zitadel.createAuthorizationURL(state, codeVerifier, scopes); | ||
``` | ||
|
||
```ts | ||
import { decodeIdToken } from "arctic"; | ||
|
||
const tokens = await zitadel.validateAuthorizationCode(code, codeVerifier); | ||
const idToken = tokens.idToken(); | ||
const claims = decodeIdToken(idToken); | ||
``` | ||
|
||
### Get user profile | ||
|
||
With the right scopes, you can then fetch the user profile like this: | ||
|
||
```ts | ||
const profileResponse = await fetch( | ||
`${env.ZITADEL_URL}/auth/v1/users/me`, | ||
{ | ||
headers: { | ||
Accept: "application/json", | ||
Authorization: `Bearer ${tokens.accessToken()}`, | ||
}, | ||
}, | ||
); | ||
``` | ||
|
||
## Revoke tokens | ||
|
||
Revoke tokens with `revokeToken()`. You should be able to revoke both the refresh and access token. It throws the same errors as `validateAuthorizationCode()`. | ||
|
||
```ts | ||
try { | ||
await box.revokeToken(refreshToken); | ||
} catch (e) { | ||
// Handle errors | ||
} | ||
``` |
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
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
import { createS256CodeChallenge, type OAuth2Tokens } from "../oauth2.js"; | ||
import { | ||
createOAuth2Request, | ||
sendTokenRequest, | ||
sendTokenRevocationRequest, | ||
} from "../request.js"; | ||
|
||
/** | ||
* Zitadel is a class that provides a simple interface to interact with the Zitadel OAuth2 API. | ||
* It's designed to be used in a server-side environment, such as a Node.js server with a configuration | ||
* of a client that has Authentication as None. This is the recommended configuration for | ||
* Zitadel web applications and backends. | ||
* | ||
* As a result, Unlike other oauth2 providers a client secret is not required. | ||
*/ | ||
export class Zitadel { | ||
private authorizationEndpoint: string; | ||
private tokenEndpoint: string; | ||
private tokenRevocationEndpoint: string; | ||
|
||
constructor( | ||
domain: string, | ||
readonly clientId: string, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I know it's a bit verbose, but can you manually define the this.clientId = clientId
this.redirectURI = redirectURI |
||
readonly redirectURI: string, | ||
) { | ||
this.authorizationEndpoint = `${domain}/oauth/v2/authorize`; | ||
this.tokenEndpoint = `${domain}/oauth/v2/token`; | ||
this.tokenRevocationEndpoint = `${domain}/oauth/v2/revoke`; | ||
} | ||
|
||
public createAuthorizationURL( | ||
state: string, | ||
codeVerifier: string, | ||
scopes: string[], | ||
): URL { | ||
const url = new URL(this.authorizationEndpoint); | ||
url.searchParams.set("response_type", "code"); | ||
url.searchParams.set("client_id", this.clientId); | ||
url.searchParams.set("state", state); | ||
url.searchParams.set("scope", scopes.join(" ")); | ||
url.searchParams.set("redirect_uri", this.redirectURI); | ||
const codeChallenge = createS256CodeChallenge(codeVerifier); | ||
url.searchParams.set("code_challenge_method", "S256"); | ||
url.searchParams.set("code_challenge", codeChallenge); | ||
return url; | ||
} | ||
|
||
public async validateAuthorizationCode( | ||
code: string, | ||
codeVerifier: string, | ||
): Promise<OAuth2Tokens> { | ||
const body = new URLSearchParams(); | ||
body.set("grant_type", "authorization_code"); | ||
body.set("code", code); | ||
body.set("code_verifier", codeVerifier); | ||
body.set("redirect_uri", this.redirectURI); | ||
body.set("client_id", this.clientId); | ||
const request = createOAuth2Request(this.tokenEndpoint, body); | ||
const tokens = await sendTokenRequest(request); | ||
return tokens; | ||
} | ||
|
||
public async refreshAccessToken(refreshToken: string): Promise<OAuth2Tokens> { | ||
const body = new URLSearchParams(); | ||
body.set("grant_type", "refresh_token"); | ||
body.set("refresh_token", refreshToken); | ||
const request = createOAuth2Request(this.tokenEndpoint, body); | ||
const tokens = await sendTokenRequest(request); | ||
return tokens; | ||
} | ||
|
||
public async revokeToken(token: string): Promise<void> { | ||
const body = new URLSearchParams(); | ||
const endpoint = new URL(this.tokenRevocationEndpoint); | ||
endpoint.searchParams.set("token", token); | ||
endpoint.searchParams.set("client_id", this.clientId); | ||
const request = createOAuth2Request(endpoint.toString(), body); | ||
await sendTokenRevocationRequest(request); | ||
} | ||
} |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you rename this to
baseURL
?