diff --git a/.changeset/soft-moose-read.md b/.changeset/soft-moose-read.md new file mode 100644 index 0000000000..c52275e7f4 --- /dev/null +++ b/.changeset/soft-moose-read.md @@ -0,0 +1,5 @@ +--- +'@forgerock/device-client': patch +--- + +Makes `realm` optional in device profile query diff --git a/e2e/davinci-app/components/fido.ts b/e2e/davinci-app/components/fido.ts index 37fd16cf02..6c68e1bafc 100644 --- a/e2e/davinci-app/components/fido.ts +++ b/e2e/davinci-app/components/fido.ts @@ -9,6 +9,7 @@ import type { FidoRegistrationCollector, FidoAuthenticationCollector, Updater, + FidoClient, } from '@forgerock/davinci-client/types'; export default function fidoComponent( @@ -17,7 +18,7 @@ export default function fidoComponent( updater: Updater, submitForm: () => Promise, ) { - const fidoApi = fido(); + const fidoApi: FidoClient = fido(); if (collector.type === 'FidoRegistrationCollector') { const button = document.createElement('button'); button.type = 'button'; diff --git a/e2e/device-client-app/src/utils/index.ts b/e2e/device-client-app/src/utils/index.ts index a386193545..d7b246ec9b 100644 --- a/e2e/device-client-app/src/utils/index.ts +++ b/e2e/device-client-app/src/utils/index.ts @@ -1,4 +1,5 @@ import { deviceClient } from '@forgerock/device-client'; +import type { ConfigOptions, DeviceClient } from '@forgerock/device-client/types'; import { CallbackType, Config, @@ -76,11 +77,8 @@ export const LoginAndGetClient = Effect.gen(function* () { const un = url.searchParams.get('un') || 'devicetestuser'; const pw = url.searchParams.get('pw') || 'password'; - const config = { + const config: ConfigOptions = { realmPath, - tree, - clientId: 'WebOAuthClient', - scope: 'profile email me.read openid', serverConfig: { baseUrl: amUrl, timeout: 3000, @@ -120,7 +118,7 @@ export const LoginAndGetClient = Effect.gen(function* () { Effect.flatMap(() => getTokens), ); - const client = deviceClient(config); + const client: DeviceClient = deviceClient(config); return client; }); diff --git a/e2e/oidc-app/src/utils/oidc-app.ts b/e2e/oidc-app/src/utils/oidc-app.ts index a95f7daf4e..69289580a0 100644 --- a/e2e/oidc-app/src/utils/oidc-app.ts +++ b/e2e/oidc-app/src/utils/oidc-app.ts @@ -12,6 +12,7 @@ import type { GenericError, GetAuthorizationUrlOptions, OauthTokens, + OidcClient, TokenExchangeErrorResponse, } from '@forgerock/oidc-client/types'; @@ -49,7 +50,7 @@ export async function oidcApp({ config, urlParams }) { const state = urlParams.get('state'); const piflow = urlParams.get('piflow'); - const oidcClient = await oidc({ config }); + const oidcClient: OidcClient = await oidc({ config }); if ('error' in oidcClient) { displayError(oidcClient); } diff --git a/e2e/protect-app/src/protect-native.ts b/e2e/protect-app/src/protect-native.ts index 94a4cb8af5..9e76487f03 100644 --- a/e2e/protect-app/src/protect-native.ts +++ b/e2e/protect-app/src/protect-native.ts @@ -9,6 +9,7 @@ import './style.css'; import { protect } from '@forgerock/protect'; +import type { Protect } from '@forgerock/protect/types'; import { CallbackType, Config, @@ -23,7 +24,7 @@ import { UserManager, } from '@forgerock/javascript-sdk'; -const protectAPI = protect({ envId: '02fb4743-189a-4bc7-9d6c-a919edfe6447' }); +const protectAPI: Protect = protect({ envId: '02fb4743-189a-4bc7-9d6c-a919edfe6447' }); const FATAL = 'Fatal'; // Check URL for query parameters diff --git a/packages/device-client/README.md b/packages/device-client/README.md index 211b13dd39..c096b246a6 100644 --- a/packages/device-client/README.md +++ b/packages/device-client/README.md @@ -35,7 +35,8 @@ npm install @forgerock/device-client --save To configure the `deviceClient`, you need to provide a `ConfigOptions` object that includes the base URL for the server and the realm path. ```typescript -import { deviceClient, type ConfigOptions } from '@forgerock/device-client'; +import { deviceClient } from '@forgerock/device-client'; +import type { ConfigOptions } from '@forgerock/device-client/types'; const config: ConfigOptions = { serverConfig: { diff --git a/packages/device-client/src/lib/types/profile-device.types.ts b/packages/device-client/src/lib/types/profile-device.types.ts index f3687bc4c2..633cecdfeb 100644 --- a/packages/device-client/src/lib/types/profile-device.types.ts +++ b/packages/device-client/src/lib/types/profile-device.types.ts @@ -5,7 +5,7 @@ * of the MIT license. See the LICENSE file for details. */ export interface GetProfileDevices { - realm: string; + realm?: string; userId: string; } diff --git a/packages/journey-client/README.md b/packages/journey-client/README.md index b00129095e..910ca9462e 100644 --- a/packages/journey-client/README.md +++ b/packages/journey-client/README.md @@ -38,8 +38,7 @@ yarn add @forgerock/journey-client ## Quick Start ```typescript -import { journey } from '@forgerock/journey-client'; -import { callbackType } from '@forgerock/sdk-types'; +import { journey, callbackType } from '@forgerock/journey-client'; async function authenticateUser() { // Initialize the client with wellknown discovery diff --git a/packages/oidc-client/README.md b/packages/oidc-client/README.md index a959f5bb16..e75122d16b 100644 --- a/packages/oidc-client/README.md +++ b/packages/oidc-client/README.md @@ -4,24 +4,292 @@ A generic OpenID Connect (OIDC) client library for JavaScript and TypeScript, de The oidc module follows the [OIDC](https://openid.net/specs/openid-connect-core-1_0.html) specification and provides a simple and easy-to-use API to interact with the OIDC server. It allows you to authenticate, retrieve the access token, revoke the token, and sign out from the OIDC server. +## Table of Contents + +- [Installation](#installation) +- [Initialization](#initialization) + - [Configuration Options](#configuration-options) +- [Quick Start](#quick-start) +- [API Reference](#api-reference) + - [authorize](#authorize) + - [token](#token) + - [user](#user) +- [Usage Examples](#usage-examples) + - [Redirect-Based Login](#redirect-based-login-authorizeurl) + - [Background Authorization](#background-authorization-authorizebackground) + - [Automatic Token Renewal](#automatic-token-renewal) + - [Error Handling](#error-handling) + +## Installation + +```bash +pnpm add @forgerock/oidc-client +# or +npm install @forgerock/oidc-client +# or +yarn add @forgerock/oidc-client +``` + +## Initialization + +```typescript +import { oidc } from '@forgerock/oidc-client'; +import { OidcConfig, OidcClient } from '@forgerock/oidc-client/types'; + +const config: OidcConfig = { + serverConfig: { wellknown: 'https://example.com/.well-known/openid-configuration' }, + clientId: 'example-client-id', + redirectUri: 'https://example-app/redirect-uri', + scope: 'openid profile email', +}; + +const oidcClient: OidcClient = await oidc({ config }); +``` + +### Configuration Options + +The `oidc()` initialization function accepts the following configuration: + +- **serverConfig** (required) + - **wellknown** (required) - URL to the OIDC provider's well-known configuration endpoint +- **clientId** (required) - Your application's client ID registered with the OIDC provider +- **redirectUri** (required) - The URI where the OIDC provider will redirect after authentication +- **scope** (required) - Space-separated list of requested scopes (e.g., `'openid profile email'`) +- **storage** (optional) - Storage configuration for tokens (defaults to localStorage) +- **timeout** (optional) - Request timeout in milliseconds +- **additionalParameters** (optional) - Additional parameters to include in authorization requests + +## Quick Start + +Here's a minimal example to get started: + +```js +import { oidc } from '@forgerock/oidc-client'; + +// Initialize the client +const oidcClient = await oidc({ config }); + +// Start authorization in the background +const authResponse = await oidcClient.authorize.background(); + +// Get tokens +const tokens = await oidcClient.token.exchange(authResponse.code, authResponse.state); + +// Get user information +const user = await oidcClient.user.info(); + +// Clean up: logout and revoke tokens +await oidcClient.user.logout(); +``` + +## API Reference + +### authorize + +Methods for creating and handling authorization flows. + +#### `authorize.url(options?)` + +Creates an authorization URL with the provided options or defaults from the configuration. + +- **Parameters**: `GetAuthorizationUrlOptions` (optional) +- **Returns**: `Promise` - The authorization URL or an error + +```js +const authUrl = await oidcClient.authorize.url(); +``` + +#### `authorize.background(options?)` + +Initiates the authorization process in the background, returning the authorization code and state or an error. This method handles the authorization flow without requiring user interaction. + +- **Parameters**: `GetAuthorizationUrlOptions` (optional) +- **Returns**: `Promise` - An object containing `code` and `state` on success, or error details on failure + +```js +const authResponse = await oidcClient.authorize.background(); +``` + +### token + +Methods for managing OAuth tokens. + +#### `token.exchange(code, state, options?)` + +Exchanges an authorization code for tokens using the token endpoint from the wellknown configuration. The tokens are automatically stored in the configured storage. + +- **Parameters**: + - `code` (string) - The authorization code received from the authorization server + - `state` (string) - The state parameter from the authorization URL creation + - `options` (`Partial`, optional) - Storage configuration for persisting tokens +- **Returns**: `Promise` - The new tokens or an error + +```js +const tokens = await oidcClient.token.exchange(authCode, authState); +``` + +#### `token.get(options?)` + +Retrieves the current OAuth tokens from storage. Optionally auto-renews tokens if they are expired or if `backgroundRenew` is enabled. + +- **Parameters**: `GetTokensOptions` (optional) + - `forceRenew` - Force token renewal even if not expired + - `backgroundRenew` - Automatically renew expired tokens + - `authorizeOptions` - Options for authorization during renewal + - `storageOptions` - Storage configuration options +- **Returns**: `Promise` - The tokens or an error + ```js -// Initialize OIDC Client -const oidcClient = await oidc({ - /* config */ +const tokens = await oidcClient.token.get(); +``` + +#### `token.revoke()` + +Revokes the access token using the revocation endpoint from the wellknown configuration. Requires an access token stored in the configured storage. + +- **Parameters**: None +- **Returns**: `Promise` - Confirmation of revocation or an error + +```js +const response = await oidcClient.token.revoke(); +``` + +### user + +Methods for user information and session management. + +#### `user.info()` + +Retrieves user information using the userinfo endpoint from the wellknown configuration. Requires an access token stored in the configured storage. + +- **Parameters**: None +- **Returns**: `Promise` - User information object or an error + +```js +const user = await oidcClient.user.info(); +``` + +#### `user.logout()` + +Logs out the user by revoking tokens and clearing the storage. Uses the end session endpoint from the wellknown configuration. + +- **Parameters**: None +- **Returns**: `Promise` - Confirmation of logout or an error + +```js +const logoutResponse = await oidcClient.user.logout(); +``` + +## Usage Examples + +### Redirect-Based Login (`authorize.url()`) + +Here's a practical example of implementing a redirect-based authentication flow. The user is redirected to the OIDC provider's login page: + +```js +import { oidc } from '@forgerock/oidc-client'; + +// 1. Initialize the client +const oidcClient = await oidc({ config }); + +// 2. Generate authorization URL and redirect user to OIDC provider +const authUrl = await oidcClient.authorize.url(); +if (typeof authUrl !== 'string' && 'error' in authUrl) { + console.error('Failed to generate authorization URL:', authUrl.error); +} else { + // Redirect to OIDC provider's login page + window.location.assign(authUrl); +} + +// After user logs in and is redirected back to your app with authorization code +// 3. Exchange authorization code for tokens +const urlParams = new URLSearchParams(window.location.search); +const code = urlParams.get('code'); +const state = urlParams.get('state'); + +const tokens = await oidcClient.token.exchange(code, state); +if ('error' in tokens) { + console.error('Failed to exchange code for tokens:', tokens.error); +} + +// 4. Retrieve user information +const userInfo = await oidcClient.user.info(); +if ('error' in userInfo) { + console.error('Failed to fetch user info:', userInfo.error); +} + +// 5. Later, when user wants to logout +const logoutResult = await oidcClient.user.logout(); +if ('error' in logoutResult) { + console.error('Logout failed:', logoutResult.error); +} +``` + +### Background Authorization (`authorize.background()`) + +Here's an example of initiating the authorization process in the background without user interaction. This method returns the authorization code and state directly: + +```js +import { oidc } from '@forgerock/oidc-client'; + +// 1. Initialize the client +const oidcClient = await oidc({ config }); + +// 2. Start authorization in the background +const authResponse = await oidcClient.authorize.background(); +if ('error' in authResponse) { + console.error('Background authorization failed:', authResponse.error); +} else { + // 3. Exchange the authorization code for tokens + const tokens = await oidcClient.token.exchange(authResponse.code, authResponse.state); + if ('error' in tokens) { + console.error('Failed to exchange code for tokens:', tokens.error); + } + + // 4. Retrieve user information + const userInfo = await oidcClient.user.info(); + if ('error' in userInfo) { + console.error('Failed to fetch user info:', userInfo.error); + } + + // 5. Later, when user wants to logout + const logoutResult = await oidcClient.user.logout(); + if ('error' in logoutResult) { + console.error('Logout failed:', logoutResult.error); + } +} +``` + +### Automatic Token Renewal + +Use automatic token renewal to keep the user's session valid. With the `backgroundRenew` option, this will either return valid tokens from storage if they exist or fetch new tokens if they are expired. + +```js +// Get tokens with automatic renewal if expired +const tokens = await oidcClient.token.get({ + backgroundRenew: true, }); -// Authorize API -const authResponse = await oidcClient.authorize.background(); // Returns code and state if successful, error if not -const authUrl = await oidcClient.authorize.url(); // Returns Auth URL or error +if ('error' in tokens) { + console.error('Failed to retrieve tokens:', tokens.error); +} else { + console.log('Access token:', tokens.access_token); +} +``` + +### Error Handling -// Tokens API -const newTokens = await oidcClient.token.exchange({ - /* code, state */ -}); // Returns new tokens or error -const existingTokens = await oidcClient.token.get(); // Returns existing tokens or error -const response = await oidcClient.token.revoke(); // Revokes an access token and returns the response or an error +The library uses a consistent error handling pattern. All methods return either a success response or an error object. Check if the response contains an `error` property: -// User API -const user = await oidcClient.user.info(); // Returns user object or error -const logoutResponse = await oidcClient.user.logout(); // Logs the user out and returns the response or an error +```js +// Pattern for handling responses +const result = await oidcClient.user.info(); +if ('error' in result) { + // Handle error case + console.error('Error:', result.error); + console.error('Error description:', result.error_description); +} else { + // Handle success case + console.log('User:', result); +} ```