Skip to content

Commit

Permalink
feat: option to use OAuth discovery endpoint for config
Browse files Browse the repository at this point in the history
  • Loading branch information
soofstad committed Nov 11, 2024
1 parent 1a5b45e commit 5cdf52a
Show file tree
Hide file tree
Showing 5 changed files with 65 additions and 10 deletions.
1 change: 1 addition & 0 deletions src/AuthContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ export const AuthProvider = ({ authConfig, children }: IAuthProvider) => {
setRefreshTokenExpire(undefined)
setIdToken(undefined)
setLoginInProgress(false)
localStorage.removeItem(`${config.storageKeyPrefix}well_known`)
}

function logOut(state?: string, logoutHint?: string, additionalParameters?: TPrimitiveRecord) {
Expand Down
6 changes: 4 additions & 2 deletions src/authConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export function createInternalConfig(passedConfig: TAuthConfig): TInternalConfig
clearURL = true,
decodeToken = true,
scope = undefined,
oauthDiscoveryEndpoint = undefined,
preLogin = () => null,
postLogin = () => null,
loginMethod = 'redirect',
Expand All @@ -26,6 +27,7 @@ export function createInternalConfig(passedConfig: TAuthConfig): TInternalConfig
const config: TInternalConfig = {
...passedConfig,
autoLogin: autoLogin,
oauthDiscoveryEndpoint: oauthDiscoveryEndpoint,
clearURL: clearURL,
decodeToken: decodeToken,
scope: scope,
Expand All @@ -46,11 +48,11 @@ export function createInternalConfig(passedConfig: TAuthConfig): TInternalConfig
export function validateConfig(config: TInternalConfig) {
if (stringIsUnset(config?.clientId))
throw Error("'clientId' must be set in the 'AuthConfig' object passed to 'react-oauth2-code-pkce' AuthProvider")
if (stringIsUnset(config?.authorizationEndpoint))
if (stringIsUnset(config?.authorizationEndpoint) && stringIsUnset(config?.oauthDiscoveryEndpoint))
throw Error(
"'authorizationEndpoint' must be set in the 'AuthConfig' object passed to 'react-oauth2-code-pkce' AuthProvider"
)
if (stringIsUnset(config?.tokenEndpoint))
if (stringIsUnset(config?.tokenEndpoint) && stringIsUnset(config?.oauthDiscoveryEndpoint))
throw Error(
"'tokenEndpoint' must be set in the 'AuthConfig' object passed to 'react-oauth2-code-pkce' AuthProvider"
)
Expand Down
51 changes: 46 additions & 5 deletions src/authentication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,28 @@ import type {
TTokenRequestForRefresh,
TTokenRequestWithCodeAndVerifier,
TTokenResponse,
TWellKnownConfig,
} from './types'

const codeVerifierStorageKey = 'PKCE_code_verifier'
const stateStorageKey = 'ROCP_auth_state'

async function getPublicWellKnownConfig(config: TInternalConfig): Promise<TWellKnownConfig> {
if (!config.oauthDiscoveryEndpoint) throw Error('No oauthDiscoveryEndpoint provided')
const storedConfig = localStorage.getItem(`${config.storageKeyPrefix}well_known`)
if (storedConfig) {
return new Promise((resolve) => resolve(JSON.parse(storedConfig)))
}
return fetch(config.oauthDiscoveryEndpoint).then(async (response) => {
if (!response.ok) {
throw Error('Failed to fetch public well-known config')
}
const fetchedConfig = (await response.json()) as TWellKnownConfig
localStorage.setItem(`${config.storageKeyPrefix}well_known`, JSON.stringify(fetchedConfig))
return fetchedConfig
})
}

export async function redirectToLogin(
config: TInternalConfig,
customState?: string,
Expand All @@ -26,7 +43,7 @@ export async function redirectToLogin(
storage.setItem(codeVerifierStorageKey, codeVerifier)

// Hash and Base64URL encode the code_verifier, used as the 'code_challenge'
return generateCodeChallenge(codeVerifier).then((codeChallenge) => {
return generateCodeChallenge(codeVerifier).then(async (codeChallenge) => {
// Set query parameters and redirect user to OAuth2 authentication endpoint
const params = new URLSearchParams({
response_type: 'code',
Expand All @@ -49,7 +66,10 @@ export async function redirectToLogin(
params.append('state', state)
}

const loginUrl = `${config.authorizationEndpoint}?${params.toString()}`
let loginUrl = config.authorizationEndpoint
if (config.oauthDiscoveryEndpoint) loginUrl = (await getPublicWellKnownConfig(config)).authorization_endpoint

loginUrl = `${loginUrl}?${params.toString()}`

// Call any preLogin function in authConfig
if (config?.preLogin) config.preLogin()
Expand Down Expand Up @@ -116,7 +136,14 @@ export const fetchTokens = (config: TInternalConfig): Promise<TTokenResponse> =>
// TODO: Remove in 2.0
...config.extraAuthParams,
}
return postTokenRequest(config.tokenEndpoint, tokenRequest, config.tokenRequestCredentials)

const tokenEndpoint = config.tokenEndpoint
if (config.oauthDiscoveryEndpoint)
return getPublicWellKnownConfig(config).then((wellKnownConfig) =>
postTokenRequest(wellKnownConfig.token_endpoint, tokenRequest, config.tokenRequestCredentials)
)

return postTokenRequest(tokenEndpoint, tokenRequest, config.tokenRequestCredentials)
}

export const fetchWithRefreshToken = (props: {
Expand All @@ -132,7 +159,13 @@ export const fetchWithRefreshToken = (props: {
...config.extraTokenParameters,
}
if (config.refreshWithScope) refreshRequest.scope = config.scope
return postTokenRequest(config.tokenEndpoint, refreshRequest, config.tokenRequestCredentials)
const tokenEndpoint = config.tokenEndpoint
if (config.oauthDiscoveryEndpoint)
return getPublicWellKnownConfig(config).then((wellKnownConfig) =>
postTokenRequest(wellKnownConfig.token_endpoint, refreshRequest, config.tokenRequestCredentials)
)

return postTokenRequest(tokenEndpoint, refreshRequest, config.tokenRequestCredentials)
}

export function redirectToLogout(
Expand All @@ -156,7 +189,15 @@ export function redirectToLogout(
if (idToken) params.append('id_token_hint', idToken)
if (state) params.append('state', state)
if (logoutHint) params.append('logout_hint', logoutHint)
window.location.assign(`${config.logoutEndpoint}?${params.toString()}`)

let logoutEndpoint = config.logoutEndpoint
if (config.oauthDiscoveryEndpoint)
// TODO: This now removes the option to disable "true" logout. Make it configurable?
return getPublicWellKnownConfig(config).then((wellKnownConfig) => {
logoutEndpoint = wellKnownConfig.revocation_endpoint
})

window.location.assign(`${logoutEndpoint}?${params.toString()}`)
}

export function validateState(urlParams: URLSearchParams, storageType: TInternalConfig['storage']) {
Expand Down
7 changes: 4 additions & 3 deletions src/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,10 @@ import { AuthContext, AuthProvider } from './AuthContext'
/** @type {import('./types').TAuthConfig} */
const authConfig = {
clientId: 'account',
authorizationEndpoint: 'https://keycloak.ofstad.xyz/realms/master/protocol/openid-connect/auth',
tokenEndpoint: 'https://keycloak.ofstad.xyz/realms/master/protocol/openid-connect/token',
logoutEndpoint: 'https://keycloak.ofstad.xyz/realms/master/protocol/openid-connect/logout',
oauthDiscoveryEndpoint: 'https://keycloak.ofstad.xyz/realms/master/.well-known/openid-configuration',
// authorizationEndpoint: 'https://keycloak.ofstad.xyz/realms/master/protocol/openid-connect/auth',
// tokenEndpoint: 'https://keycloak.ofstad.xyz/realms/master/protocol/openid-connect/token',
// logoutEndpoint: 'https://keycloak.ofstad.xyz/realms/master/protocol/openid-connect/logout',
redirectUri: 'http://localhost:5173/',
onRefreshTokenExpire: (event) => event.logIn('', {}, 'popup'),
preLogin: () => console.log('Logging in...'),
Expand Down
10 changes: 10 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@ export type TTokenData = {
[x: string]: any
}

export type TWellKnownConfig = {
authorization_endpoint: string
token_endpoint: string
revocation_endpoint: string
// biome-ignore lint: It really can be `any` (almost)
[x: string]: any
}

export type TTokenResponse = {
access_token: string
scope: string
Expand Down Expand Up @@ -66,6 +74,7 @@ export type TAuthConfig = {
clientId: string
authorizationEndpoint: string
tokenEndpoint: string
oauthDiscoveryEndpoint?: string
redirectUri: string
scope?: string
state?: string
Expand Down Expand Up @@ -103,6 +112,7 @@ export type TInternalConfig = {
clientId: string
authorizationEndpoint: string
tokenEndpoint: string
oauthDiscoveryEndpoint?: string
redirectUri: string
scope?: string
state?: string
Expand Down

0 comments on commit 5cdf52a

Please sign in to comment.