Skip to content
This repository was archived by the owner on Mar 18, 2025. It is now read-only.

Commit 0560b79

Browse files
authored
Feature/sso gateway (#1)
* extract organization domain from refresh if pattern present * use domain from authorization redirect param if present for token url generation * build sso gateway url depending on params * missing change for previous cmmit * integrate signinWithSSOGateway hook in provider * adapt logout revoke process with new refresh pattern * adapt refresh process with new refresh token pattern * add test for token url with organziation domain present in authorization * test revokeTokenUrl with orgnaization domain pattern in refresh * add refresh token url when organization domain present * test ssoGatewayUrl genration depending on idpId value * syntax * create component to handle gatewayprocess * handle error callback in sso button * add testing and check for gateway button component * begin integration of dedicated_server param into cryptrconfig * use dedicated_server to build sso gateway url * add dedicated_server to config template * add another idp to gateway multiple button * fix typo in test * set type to result of user method * update README to match new Cryptr config and gateway elements
1 parent 0fd2691 commit 0560b79

15 files changed

+744
-33
lines changed

README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ const config: CryptrConfig = {
9797
default_redirect_uri: 'cryptr://YOUR_TENANT',
9898
region: Region.EU,
9999
cryptr_base_url: 'YOUR_SERVER_URL',
100+
dedicated_server: true, // if you have a dedicated server on cryptr
100101
};
101102
```
102103

@@ -200,6 +201,22 @@ const { signinWithSSO } = useCryptr()
200201
signinWithSSO(idpID: string, successCallback?: (data: any) => any, errorCallback?: (data: any) => any)
201202
```
202203

204+
#### signinWithSSOGateway
205+
206+
Hook action to sign in the user using Cryptr Gateway when you don't know what is the precise ID used by your end user. You can precise a subset of IDP_ID that user will be able to consume, if none end-user will be able to find his by email or organziation_domain.
207+
208+
When you set one string for `idpId` behavior is same as `signinWithSSO`
209+
210+
:warning: requires a proper setup of you different organization
211+
212+
213+
```js
214+
const { signinWithSSOGateway } = useCryptr()
215+
216+
// [...]
217+
signinWithSSOGateway(idpID?: string | string[], successCallback?: (data: any) => any, errorCallback?: (data: any) => any)
218+
```
219+
203220
#### refreshTokens
204221

205222
Hook action to refresh tokens to new ones.
@@ -251,5 +268,7 @@ const { isLoading } = useCryptr()
251268
This SDK also includes Component to simplify your integration.
252269

253270
- `SsoSigInButton` to login using SSO (hides when session is already active [`autoHide={false}` to disable])
271+
272+
- `SsoGatewayButton` to login using SSO Gateway (hides when session is already active [`autoHide={false}` to disable])
254273
- `LogOutButton` to logout user (hides when no session is active [`autoHide={false}` to disable])
255274
- `RefreshButton` to get new tokens (hides when session is already active [`autoHide={false}` to disable])

example/cryptrConfig.template.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ export const cryptrConfig: CryptrConfig = {
77
default_redirect_uri: 'cryptr://your-app',
88
region: Region.EU,
99
cryptr_base_url: 'YOUR_CRYPTR_SERVER_URL',
10+
dedicated_server: true,
1011
};
1112

1213
export const IDP_ID = 'YOUR_IDP_ID';
14+
export const IDP_ID2 = 'YOUR_SECOND_IDP_ID';

example/src/components/AuthenticatedView.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Button, Text } from 'react-native';
33
import {
44
LogOutButton,
55
RefreshButton,
6+
SsoGatewayButton,
67
SsoSignInButton,
78
useCryptr,
89
} from '@cryptr/cryptr-react-native';
@@ -46,9 +47,13 @@ const AuthenticatedView = () => {
4647
<>
4748
<Text style={styles.textAuthenticated}>You're logged in</Text>
4849
<HorizontalDivider />
50+
{user() && (
51+
<TokenView title={user()!.tnt} value={`Issued at ${user()!.iat}`} />
52+
)}
4953
{accessToken && <TokenView title="Access Token" value={accessToken} />}
5054
{user() && <TokenView title="User" value={JSON.stringify(user())} />}
5155
<HorizontalDivider />
56+
<SsoGatewayButton text="Gateway" />
5257
<SsoSignInButton idpId={IDP_ID} autoHide={false} />
5358
<LogOutButton
5459
successCallback={logOutCallback}

example/src/components/UnauthenticatedView.tsx

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
import React from 'react';
2-
import { StyleSheet } from 'react-native';
3-
import { SsoSignInButton } from '@cryptr/cryptr-react-native';
4-
import { IDP_ID } from '../../cryptrConfig.template';
2+
import { Pressable, StyleSheet, Text, View } from 'react-native';
3+
import {
4+
SsoGatewayButton,
5+
SsoSignInButton,
6+
useCryptr,
7+
} from '@cryptr/cryptr-react-native';
8+
import { IDP_ID, IDP_ID2 } from '../../cryptrConfig.template';
9+
import { styles } from '../styles';
10+
import HorizontalDivider from './HorizontalDivider';
511

612
export const unauthStyles = StyleSheet.create({
713
ssoBtnContainer: {
@@ -18,9 +24,25 @@ export const unauthStyles = StyleSheet.create({
1824
});
1925

2026
const UnauthenticatedView = () => {
27+
const { signinWithSSOGateway } = useCryptr();
28+
2129
return (
2230
<>
2331
<SsoSignInButton idpId={IDP_ID} />
32+
<HorizontalDivider />
33+
<SsoGatewayButton autoHide={false} text="Gateway" />
34+
<View>
35+
<Pressable
36+
onPress={() => signinWithSSOGateway(IDP_ID)}
37+
style={styles.button}
38+
>
39+
<Text>Gateway on IDP</Text>
40+
</Pressable>
41+
<SsoGatewayButton
42+
idpId={[IDP_ID, IDP_ID2]}
43+
text="Gateway with multiple IDPs"
44+
/>
45+
</View>
2446
</>
2547
);
2648
};
Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
import React from 'react';
2+
import CryptrProvider from '../../models/CryptrProvider';
3+
import renderer from 'react-test-renderer';
4+
import type { CryptrConfig } from '../../utils/interfaces';
5+
import CryptrSsoGatewayButton from '../../components/CryptrSsoGatewayButton';
6+
import { Locale } from '../../utils/enums';
7+
import { Text } from 'react-native';
8+
import { render, fireEvent } from '@testing-library/react-native';
9+
import Cryptr from '../../models/Cryptr';
10+
11+
interface JsonTree {
12+
children: any;
13+
}
14+
15+
jest.mock('../../models/Cryptr', () => ({
16+
startSecuredView: jest.fn(),
17+
getRefresh: jest.fn(),
18+
setRefresh: jest.fn(),
19+
}));
20+
21+
describe('CryptrSsoGatewayButton', () => {
22+
const config: CryptrConfig = {
23+
tenant_domain: 'shark_academy',
24+
client_id: '123',
25+
audience: 'cryptr://audience',
26+
default_redirect_uri: 'cryptr://redirect-uri',
27+
};
28+
29+
it('should mount in provider', () => {
30+
const tree = renderer.create(
31+
<CryptrProvider {...config}>
32+
<CryptrSsoGatewayButton />
33+
</CryptrProvider>
34+
);
35+
36+
expect(tree).not.toBeUndefined();
37+
const jsonTree = tree.toJSON() as JsonTree;
38+
expect(jsonTree.children[0].children).toEqual(['Sign in with SSO']);
39+
expect(tree.root.findByType(CryptrSsoGatewayButton).props).toEqual({});
40+
});
41+
42+
it('should mount in provider and allow french locale', () => {
43+
const tree = renderer.create(
44+
<CryptrProvider {...config} default_locale={Locale.FR}>
45+
<CryptrSsoGatewayButton />
46+
</CryptrProvider>
47+
);
48+
49+
expect(tree).not.toBeUndefined();
50+
const jsonTree = tree.toJSON() as JsonTree;
51+
expect(jsonTree.children[0].children).toEqual(['Se connecter en SSO']);
52+
expect(tree.root.findByType(CryptrSsoGatewayButton).props).toEqual({});
53+
});
54+
55+
it('should mount in provider and allow non blank idpId string', () => {
56+
const tree = renderer.create(
57+
<CryptrProvider {...config}>
58+
<CryptrSsoGatewayButton idpId={'shark_academy_12fregerg'} />
59+
</CryptrProvider>
60+
);
61+
62+
expect(tree).not.toBeUndefined();
63+
const jsonTree = tree.toJSON() as JsonTree;
64+
expect(jsonTree.children[0].children).toEqual(['Sign in with SSO']);
65+
expect(tree.root.findByType(CryptrSsoGatewayButton).props).toEqual({
66+
idpId: 'shark_academy_12fregerg',
67+
});
68+
});
69+
70+
it('should mount in provider and allow non blank and non empty idpId string array', () => {
71+
const tree = renderer.create(
72+
<CryptrProvider {...config}>
73+
<CryptrSsoGatewayButton
74+
idpId={['shark_academy_12fregerg', 'company_fzef1238']}
75+
/>
76+
</CryptrProvider>
77+
);
78+
79+
expect(tree).not.toBeUndefined();
80+
const jsonTree = tree.toJSON() as JsonTree;
81+
expect(jsonTree.children[0].children).toEqual(['Sign in with SSO']);
82+
expect(tree.root.findByType(CryptrSsoGatewayButton).props).toEqual({
83+
idpId: ['shark_academy_12fregerg', 'company_fzef1238'],
84+
});
85+
});
86+
87+
it('should throw error if blank idpId string', () => {
88+
expect(() =>
89+
renderer.create(
90+
<CryptrProvider {...config}>
91+
<CryptrSsoGatewayButton idpId={''} />
92+
</CryptrProvider>
93+
)
94+
).toThrow('Please provide non blank string(s) for idpId');
95+
});
96+
97+
it('should throw error if empty idpId string array', () => {
98+
expect(() =>
99+
renderer.create(
100+
<CryptrProvider {...config}>
101+
<CryptrSsoGatewayButton idpId={[]} />
102+
</CryptrProvider>
103+
)
104+
).toThrow('Please provide non blank string(s) for idpId');
105+
});
106+
107+
it('should throw error if blank idpId string array', () => {
108+
expect(() =>
109+
renderer.create(
110+
<CryptrProvider {...config}>
111+
<CryptrSsoGatewayButton idpId={['']} />
112+
</CryptrProvider>
113+
)
114+
).toThrow('Please provide non blank string(s) for idpId');
115+
});
116+
117+
it('should throw error if one blank item in idpId string array', () => {
118+
expect(() =>
119+
renderer.create(
120+
<CryptrProvider {...config}>
121+
<CryptrSsoGatewayButton idpId={['idp_id', '']} />
122+
</CryptrProvider>
123+
)
124+
).toThrow('Please provide non blank string(s) for idpId');
125+
});
126+
127+
it('should mount in provider and custom text', () => {
128+
const tree = renderer.create(
129+
<CryptrProvider {...config}>
130+
<CryptrSsoGatewayButton text="Access to gateway" />
131+
</CryptrProvider>
132+
);
133+
134+
expect(tree).not.toBeUndefined();
135+
const jsonTree = tree.toJSON() as JsonTree;
136+
expect(jsonTree.children[0].children).toEqual(['Access to gateway']);
137+
expect(tree.root.findByType(CryptrSsoGatewayButton).props).toEqual({
138+
text: 'Access to gateway',
139+
});
140+
});
141+
142+
it('should mount in provider and allow custom children', () => {
143+
const tree = renderer.create(
144+
<CryptrProvider {...config}>
145+
<CryptrSsoGatewayButton text="Access to gateway">
146+
<Text>Custom button text</Text>
147+
</CryptrSsoGatewayButton>
148+
</CryptrProvider>
149+
);
150+
151+
expect(tree).not.toBeUndefined();
152+
const jsonTree = tree.toJSON() as JsonTree;
153+
expect(jsonTree.children[0].children).toEqual(['Custom button text']);
154+
});
155+
156+
it('should start standard gateway process on press action', () => {
157+
const { getByText } = render(
158+
<CryptrProvider {...config}>
159+
<CryptrSsoGatewayButton>
160+
<Text>Custom idp content</Text>
161+
</CryptrSsoGatewayButton>
162+
</CryptrProvider>
163+
);
164+
const item = getByText('Custom idp content');
165+
const startSecuredViewFn = jest.spyOn(Cryptr, 'startSecuredView');
166+
fireEvent.press(item);
167+
expect(startSecuredViewFn).toHaveBeenCalledWith(
168+
expect.stringContaining(
169+
'https://auth.cryptr.eu/t/shark_academy/?client_id=123'
170+
),
171+
expect.anything(),
172+
expect.anything()
173+
);
174+
expect(startSecuredViewFn).toHaveBeenCalledWith(
175+
expect.not.stringContaining('idp_id='),
176+
expect.anything(),
177+
expect.anything()
178+
);
179+
180+
expect(startSecuredViewFn).toHaveBeenCalledWith(
181+
expect.not.stringContaining('idp_ids%5B%5D='),
182+
expect.anything(),
183+
expect.anything()
184+
);
185+
186+
startSecuredViewFn.mockRestore();
187+
});
188+
189+
it('should start standard dedicated gateway process on press action', () => {
190+
const { getByText } = render(
191+
<CryptrProvider {...config} dedicated_server={true}>
192+
<CryptrSsoGatewayButton>
193+
<Text>Custom idp content</Text>
194+
</CryptrSsoGatewayButton>
195+
</CryptrProvider>
196+
);
197+
const item = getByText('Custom idp content');
198+
const startSecuredViewFn = jest.spyOn(Cryptr, 'startSecuredView');
199+
fireEvent.press(item);
200+
expect(startSecuredViewFn).toHaveBeenCalledWith(
201+
expect.stringContaining('https://auth.cryptr.eu/?client_id'),
202+
expect.anything(),
203+
expect.anything()
204+
);
205+
expect(startSecuredViewFn).toHaveBeenCalledWith(
206+
expect.not.stringContaining('idp_id='),
207+
expect.anything(),
208+
expect.anything()
209+
);
210+
211+
expect(startSecuredViewFn).toHaveBeenCalledWith(
212+
expect.not.stringContaining('idp_ids%5B%5D='),
213+
expect.anything(),
214+
expect.anything()
215+
);
216+
217+
startSecuredViewFn.mockRestore();
218+
});
219+
220+
it('should start idp gateway process on press action', () => {
221+
const { getByText } = render(
222+
<CryptrProvider {...config}>
223+
<CryptrSsoGatewayButton idpId="app_sso_idp_id">
224+
<Text>Custom idp content</Text>
225+
</CryptrSsoGatewayButton>
226+
</CryptrProvider>
227+
);
228+
const item = getByText('Custom idp content');
229+
const startSecuredViewFn = jest.spyOn(Cryptr, 'startSecuredView');
230+
fireEvent.press(item);
231+
expect(startSecuredViewFn).toHaveBeenCalledWith(
232+
expect.stringContaining('idp_id=app_sso_idp_id'),
233+
expect.anything(),
234+
expect.anything()
235+
);
236+
startSecuredViewFn.mockRestore();
237+
});
238+
239+
it('should start idps gateway process on press action', () => {
240+
const { getByText } = render(
241+
<CryptrProvider {...config}>
242+
<CryptrSsoGatewayButton idpId={['app_sso_idp_id', 'another_idp_id']}>
243+
<Text>Custom idp content</Text>
244+
</CryptrSsoGatewayButton>
245+
</CryptrProvider>
246+
);
247+
const item = getByText('Custom idp content');
248+
const startSecuredViewFn = jest.spyOn(Cryptr, 'startSecuredView');
249+
fireEvent.press(item);
250+
expect(startSecuredViewFn).toHaveBeenCalledWith(
251+
expect.stringContaining('idp_ids%5B%5D=app_sso_idp_id'),
252+
expect.anything(),
253+
expect.anything()
254+
);
255+
expect(startSecuredViewFn).toHaveBeenCalledWith(
256+
expect.stringContaining('idp_ids%5B%5D=another_idp_id'),
257+
expect.anything(),
258+
expect.anything()
259+
);
260+
startSecuredViewFn.mockRestore();
261+
});
262+
});

0 commit comments

Comments
 (0)