Skip to content

Commit d71b701

Browse files
crhistianramirezerincdustin
authored andcommitted
feat: add support for single-sign-on via openid connect authentication
includes redirecting users to the identity provider and handling the response
1 parent dfbf6be commit d71b701

File tree

10 files changed

+406
-49
lines changed

10 files changed

+406
-49
lines changed

README.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,43 @@ An **optional** [Open API specification](https://swagger.io/specification/) obje
3838
#### **defaultErrorHandler** `(error:OrderCloudError) => void`
3939
An **optional** callback function for globally handling OrderCloud errors in your application. Useful for wiring up toast-like feedback.
4040

41+
#### **openIdConnect** `object`
42+
An **optional** object containing configuration for single-sign-on via [OpenID Connect](https://ordercloud.io/knowledge-base/sso-via-openid-connect).
43+
44+
### **openIdConnect** `boolean`
45+
Set to `true` to activate single-sign-on via OpenIDConnect in your application. If `false`, all OIDC logic (such as login redirects and token handling) will be disabled, even if configs are defined.
46+
47+
#### **openIdConnect.configs** `{id: string, roles?: string[], clientId: string}[]`
48+
An array of OpenID Connect configuration objects. Each defines the settings required to authenticate against a specific identity provider. At least one configuration must be provided.
49+
50+
#### **openIdConnect.configs.[i].id** `string`
51+
The ID of the [OpenID connect configuration](https://ordercloud.io/api-reference/authentication-and-authorization/open-id-connects/create) that should be targeted for authentication
52+
53+
#### **openIdConnect.configs.[i].roles** `string`
54+
An **optional** array of roles that will be requested when authenticating. If excluded, the token generated will contain any roles assigned to the user. Unless you have a specific reason for limiting roles, we recommend omitting this option.
55+
56+
#### **openIdConnect.configs.[i].clientId** `string`
57+
An **optional** OrderCloud clientId to authenticate against. By default, will use `clientId` at the root of the provider settings.
58+
59+
#### **openIdConnect.configs.[i].appStartPath** `string`
60+
An **optional** path to redirect the user to after returning from the identity provider. See [here](https://ordercloud.io/knowledge-base/sso-via-openid-connect#deep-linking) for more information
61+
62+
#### **openIdConnect.configs.[i].customParams** `string`
63+
**optional** query parameters passed along to the `AuthorizationEndpoint`. See [here](https://ordercloud.io/knowledge-base/sso-via-openid-connect) for more information
64+
65+
#### **openIdConnect.autoRedirect** `boolean`
66+
True will automatically redirect the user to the first openIdConnect config stored if the token is expired, or invalid. This is a simplified use case. For more control, or when you need to handle multiple identity providers set this to false and handle redirect on your own by calling `loginWithOpenIdConnect`
67+
68+
### **openIdConnect.accessTokenQueryParamName** `string`
69+
Query parameter name where the OrderCloud access token is stored after login. For example, if [AppStartUrl](https://ordercloud.io/api-reference/authentication-and-authorization/open-id-connects/create) is `https://my-application.com/login?token={0}`, use `token`
70+
71+
### **openIdConnect.refreshTokenQueryParamName**
72+
The **optional** query parameter name for the refresh token after login. Example: if [AppStartUrl](https://ordercloud.io/api-reference/authentication-and-authorization/open-id-connects/create) is `https://my-application.com/login?token={0}&refresh={3}`, use `refresh`
73+
74+
75+
### **openIdConnect.idpAccessTokenQueryParamName**
76+
The **optional** query parameter name for the identity provider access token after login. Example: if [AppStartUrl](https://ordercloud.io/api-reference/authentication-and-authorization/open-id-connects/create) is `https://my-application.com/login?token={0}&idptoken={1}`, use `idptoken`
77+
4178
## `useOrderCloudContext()` hook
4279
This hook returns the OrderCloud context that the OrderCloudProvider sets up based on your provided options. If anonymous authentication is allowed the OrderCloud context will automatically be in an authenticated state on first page load (shortly after the first React lifecycle).
4380

@@ -53,6 +90,9 @@ When true, the currently active OrderCloud access token is a _registered_ user (
5390
#### **login**: `(username:string, password:string, rememberMe:boolean) => Promise<AccessToken>`
5491
An asyncrhonous callback method for building a login form for your application. When **rememberMe** is set to `true`, the `OrderCloudProvider` will attempt to store and use the `refresh_token` as long as it is valid. It is not necessary to do anything with the `AccessToken` response as this method will take care of managing the active token and authentication state for you.
5592

93+
### **loginWithOpenIdConnect**: `(openIdConnectId: string, options?: { appStartPath?: string; customParams?: string; }) => void
94+
A method for manually redirecting a user to the identity provider login page defined by the openIdConnectId. To use this method you must define the relevent `openIdConnect` properties
95+
5696
#### **logout**: `() => void`
5797
A callback for logging out a registered user from your application. This will also clear the Tanstack query client cache for OrderCloud API calls, forcing any actively used queries to refetch once anonymous auth takes over again or the user logs back in.
5898

package-lock.json

Lines changed: 5 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,8 @@
6262
"@hookform/resolvers": "^3.3.4",
6363
"@tanstack/react-query": "^5.62.2",
6464
"@tanstack/react-table": "^8.20.5",
65-
"ordercloud-javascript-sdk": "^10.0.0",
65+
"axios": "^1.1.3",
66+
"ordercloud-javascript-sdk": "^11.1.0",
6667
"react": "^18.3.1",
6768
"react-dom": "^18.3.1",
6869
"react-hook-form": "^7.53.2"

src/context.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,13 @@ const INITIAL_ORDERCLOUD_CONTEXT: IOrderCloudContext = {
99
login: async (username: string, password: string, rememberMe?: boolean) => {
1010
return Promise.reject({username, password, rememberMe})
1111
},
12+
loginWithOpenIdConnect: (
13+
openIdConnectId: string
14+
) => {
15+
throw new Error(
16+
`loginWithOpenIdConnect is not implemented. ${openIdConnectId}`
17+
);
18+
},
1219
setToken: async (accessToken: string ) => {
1320
return Promise.reject({accessToken})
1421
},

src/hooks/useOnceAtATime.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { useRef, useCallback } from 'react';
2+
3+
/**
4+
* useOnceAtATime
5+
*
6+
* A React hook that ensures an async function is only executed once at a time.
7+
* While the function is in-flight, all subsequent calls return the same Promise.
8+
* Once the function resolves or rejects, it can be called again.
9+
*
10+
* Useful for deduplicating concurrent requests (e.g. token validation, lazy loading).
11+
*
12+
* @template TArgs - Argument types of the async function
13+
* @template TResult - Return type of the async function
14+
*
15+
* @param fn - The async function to guard
16+
* @returns An object with:
17+
* - run: the deduplicated function
18+
* - isRunning: boolean indicating whether the function is currently executing
19+
*/
20+
export function useOnceAtATime<TArgs extends any[], TResult>(
21+
fn: (...args: TArgs) => Promise<TResult>
22+
) {
23+
const inFlightRef = useRef<Promise<TResult> | null>(null);
24+
25+
const run = useCallback((...args: TArgs): Promise<TResult> => {
26+
if (inFlightRef.current) return inFlightRef.current;
27+
28+
inFlightRef.current = (async () => {
29+
try {
30+
return await fn(...args);
31+
} finally {
32+
inFlightRef.current = null; // allow future calls
33+
}
34+
})();
35+
36+
return inFlightRef.current;
37+
}, [fn]);
38+
39+
const isRunning = !!inFlightRef.current;
40+
41+
return { run, isRunning };
42+
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
export interface IOpenIdConnectSettings
2+
{
3+
/**
4+
* Enables or disables OpenID Connect authentication.
5+
*
6+
* Set to `true` to activate OIDC support in your application. If `false`, all OIDC logic
7+
* (such as login redirects and token handling) will be disabled, even if configs are defined.
8+
*/
9+
enabled: boolean
10+
/**
11+
* An array of OpenID Connect configuration objects.
12+
* Each defines the settings required to authenticate against a specific identity provider.
13+
* At least one configuration must be provided.
14+
*/
15+
configs: IOpenIdConnectConfig[]
16+
/**
17+
* True will automatically redirect the user to the
18+
* first openIdConnect config stored if the token is expired, or invalid.
19+
* This is a simplified use case. For more control, or when you need to
20+
* handle multiple identity providers set this to false and handle redirect on your
21+
* own by calling `loginWithOpenIdConnect`
22+
*/
23+
autoRedirect?: boolean
24+
/**
25+
* The name of the query parameter under which the ordercloud access token will be stored under after successful login.
26+
* This will vary based on your [OpenIdConnect.AppStartUrl](https://ordercloud.io/api-reference/authentication-and-authorization/open-id-connects/create).
27+
* For example if your `AppStartUrl` is `https://my-buyer-application.com/login?token={0}` then the value should be `token`
28+
*/
29+
accessTokenQueryParamName: string
30+
/**
31+
* The **optional** name of the query parameter under which the ordercloud refresh token will be stored
32+
* under after successful login. This will vary based on your [OpenIdConnect.AppStartUrl](https://ordercloud.io/api-reference/authentication-and-authorization/open-id-connects/create).
33+
* For example if your `AppStartUrl` is `https://my-buyer-application.com/login?token={0}&refresh={3}` then the value should be `refresh`
34+
*/
35+
refreshTokenQueryParamName?: string
36+
/**
37+
* The **optional** name of the query parameter under which the idp access token will be stored
38+
* under after successful login. This will vary based on your [OpenIdConnect.AppStartUrl](https://ordercloud.io/api-reference/authentication-and-authorization/open-id-connects/create).
39+
* For example if your `AppStartUrl` is `https://my-buyer-application.com/login?token={0}&idptoken={1}` then the value should be `idptoken`
40+
*/
41+
idpAccessTokenQueryParamName?: string
42+
/**
43+
* An **optional** path to redirect the user to after returning from the identity provider.
44+
* See [here](https://ordercloud.io/knowledge-base/sso-via-openid-connect#deep-linking) for more information
45+
* This global setting will be used if not overridden by the `appStartPath` in the individual OpenID Connect configurations.
46+
* Call `setAppStartPath()` to change this value at runtime.
47+
*/
48+
appStartPath?: string
49+
/**
50+
* **optional** query parameters passed along to the `AuthorizationEndpoint`.
51+
* See [here](https://ordercloud.io/knowledge-base/sso-via-openid-connect) for more information
52+
* This global setting will be used if not overridden by the `customParams` in the individual OpenID Connect configurations.
53+
* Call `setCustomParams()` to change this value at runtime.
54+
*/
55+
customParams?: string
56+
}
57+
58+
export interface IOpenIdConnectConfig
59+
{
60+
/**
61+
* The ID of the [OpenID connect configuration](https://ordercloud.io/api-reference/authentication-and-authorization/open-id-connects/create)
62+
* that should be targeted for authentication
63+
*/
64+
id: string
65+
/**
66+
* An **optional** array of roles that will be requested when authenticating.
67+
* If excluded, the token generated will contain any roles assigned to the user.
68+
* Unless you have a specific reason for limiting roles, we recommend omitting this option.
69+
*/
70+
roles?: string[]
71+
/**
72+
* An **optional** OrderCloud clientId to authenticate against.
73+
* By default, will use `clientId` at the root of the provider settings.
74+
*/
75+
clientId?: string
76+
/**
77+
* An **optional** path to redirect the user to after returning from the identity provider.
78+
* See [here](https://ordercloud.io/knowledge-base/sso-via-openid-connect#deep-linking) for more information
79+
* call `setAppStartPath(openIdConnectConfigId)` to change this value at runtime.
80+
*/
81+
appStartPath?: string
82+
83+
/**
84+
* **optional** query parameters passed along to the `AuthorizationEndpoint`.
85+
* See [here](https://ordercloud.io/knowledge-base/sso-via-openid-connect) for more information
86+
* call `setCustomParams(openIdConnectConfigId)` to change this value at runtime.
87+
*/
88+
customParams?: string
89+
}

src/models/IOrderCloudContext.ts

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
import { AccessToken, ApiRole, OrderCloudError } from "ordercloud-javascript-sdk";
1+
import {
2+
AccessToken,
3+
ApiRole,
4+
OrderCloudError,
5+
} from "ordercloud-javascript-sdk";
26
import { IOrderCloudErrorContext } from "./IOrderCloudErrorContext";
37
import { OpenAPIV3 } from "openapi-types";
48

@@ -21,15 +25,29 @@ export interface IOrderCloudContext {
2125
/**
2226
* authenticates using the configured client ID and username / password
2327
*/
24-
login: (username:string, password:string, rememberMe?:boolean) => Promise<AccessToken>;
28+
login: (
29+
username: string,
30+
password: string,
31+
rememberMe?: boolean
32+
) => Promise<AccessToken>;
33+
/**
34+
* authenticates using the configured OpenID Connect settings
35+
*/
36+
loginWithOpenIdConnect: (
37+
openIdConnectId: string,
38+
options?: {
39+
appStartPath?: string;
40+
customParams?: string;
41+
}
42+
) => void;
2543
/**
2644
* authenticates using the provided OrderCloud access token
2745
*/
28-
setToken: (accessToken: string ) => void;
46+
setToken: (accessToken: string) => void;
2947
/**
3048
* Signifies when authorization is in a loading state
3149
*/
32-
authLoading: boolean
50+
authLoading: boolean;
3351

3452
/**
3553
* If anonymous, this will retrieve a new anon token, useful for anonymous
@@ -43,9 +61,13 @@ export interface IOrderCloudContext {
4361
scope?: ApiRole[];
4462
customScope?: string[];
4563
allowAnonymous: boolean;
46-
defaultErrorHandler?: (error:OrderCloudError, context:IOrderCloudErrorContext) => void;
64+
defaultErrorHandler?: (
65+
error: OrderCloudError,
66+
context: IOrderCloudErrorContext
67+
) => void;
4768
token?: string;
69+
idpToken?: string;
4870
xpSchemas?: OpenAPIV3.SchemaObject;
4971
autoApplyPromotions?: boolean;
50-
currencyDefaults: { currencyCode: string, language: string }
51-
}
72+
currencyDefaults: { currencyCode: string; language: string };
73+
}

src/models/IOrderCloudProvider.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
import { ApiRole, OrderCloudError, SdkConfiguration } from "ordercloud-javascript-sdk";
22
import { IOrderCloudContext } from "./IOrderCloudContext";
33
import { OpenAPIV3 } from "openapi-types";
4+
import { IOpenIdConnectSettings } from "./IOpenIdConnectSettings";
45

56
export interface IOrderCloudProvider {
67
baseApiUrl: string;
78
clientId: string;
89
scope?: ApiRole[];
910
customScope?: string[];
1011
allowAnonymous: boolean;
12+
openIdConnect?: IOpenIdConnectSettings;
1113
xpSchemas?: OpenAPIV3.SchemaObject;
1214
autoApplyPromotions?: boolean,
1315
configurationOverrides?: Omit<SdkConfiguration, 'baseApiUrl' | 'clientID'>

src/models/index.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@ import { IOrderCloudOperationObject } from "./IOrderCloudOperationObject";
22
import { IOrderCloudContext } from "./IOrderCloudContext";
33
import { IOrderCloudErrorContext } from "./IOrderCloudErrorContext";
44
import { IOrderCloudProvider } from "./IOrderCloudProvider";
5+
import { IOpenIdConnectSettings, IOpenIdConnectConfig } from "./IOpenIdConnectSettings";
56

67
export type {
78
IOrderCloudContext,
89
IOrderCloudErrorContext,
910
IOrderCloudProvider,
10-
IOrderCloudOperationObject
11+
IOrderCloudOperationObject,
12+
IOpenIdConnectSettings,
13+
IOpenIdConnectConfig
1114
}

0 commit comments

Comments
 (0)