Skip to content

Commit 8535061

Browse files
committed
Add registration tests and docs
1 parent a23d138 commit 8535061

File tree

3 files changed

+293
-2
lines changed

3 files changed

+293
-2
lines changed

.eslintrc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,5 @@ rules:
2727
eqeqeq:
2828
- error
2929
- smart
30+
max-statements:
31+
- off

README.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ with optional overrides.
123123
* **customHeaders** - (`object`) _ANDROID_ you can specify custom headers to pass during authorize request and/or token request.
124124
* **authorize** - (`{ [key: string]: value }`) headers to be passed during authorization request.
125125
* **token** - (`{ [key: string]: value }`) headers to be passed during token retrieval request.
126+
* **register** - (`{ [key: string]: value }`) headers to be passed during registration request.
126127
* **useNonce** - (`boolean`) _IOS_ (default: true) optionally allows not sending the nonce parameter, to support non-compliant providers
127128
* **usePKCE** - (`boolean`) (default: true) optionally allows not sending the code_challenge parameter and skipping PKCE code verification, to support non-compliant providers.
128129

@@ -179,6 +180,49 @@ const result = await revoke(config, {
179180
});
180181
```
181182

183+
184+
### `register`
185+
186+
This will perform [dynamic client registration](https://openid.net/specs/openid-connect-registration-1_0.html) on the given provider.
187+
If the provider supports dynamic client registration, it will generate a `clientId` for you to use in subsequent calls to this library.
188+
189+
```js
190+
import { register } from 'react-native-app-auth';
191+
192+
const registerConfig = {
193+
issuer: '<YOUR_ISSUER_URL>',
194+
redirectUrls: ['<YOUR_REDIRECT_URL>', '<YOUR_OTHER_REDIRECT_URL>'],
195+
};
196+
197+
const registerResult = await register(registerConfig);
198+
```
199+
200+
#### registerConfig
201+
202+
* **issuer** - (`string`) same as in authorization config
203+
* **serviceConfiguration** - (`object`) same as in authorization config
204+
* **redirectUrls** - (`array<string>`) _REQUIRED_ specifies all of the redirect urls that your client will use for authentication
205+
* **responseTypes** - (`array<string>`) an array that specifies which [OAuth 2.0 response types](https://openid.net/specs/oauth-v2-multiple-response-types-1_0.html) your client will use. The default value is `['code']`
206+
* **grantTypes** - (`array<string>`) an array that specifies which [OAuth 2.0 grant types](https://oauth.net/2/grant-types/) your client will use. The default value is `['authorization_code']`
207+
* **subjectType** - (`string`) requests a specific [subject type](https://openid.net/specs/openid-connect-core-1_0.html#SubjectIDTypes) for your client
208+
* **tokenEndpointAuthMethod** (`string`) specifies which `clientAuthMethod` your client will use for authentication. The default value is `'client_secret_basic'`
209+
* **additionalParameters** - (`object`) additional parameters that will be passed in the registration request.
210+
Must be string values! E.g. setting `additionalParameters: { hello: 'world', foo: 'bar' }` would add
211+
`hello=world&foo=bar` to the authorization request.
212+
* **dangerouslyAllowInsecureHttpRequests** - (`boolean`) _ANDROID_ same as in authorization config
213+
* **customHeaders** - (`object`) _ANDROID_ same as in authorization config
214+
215+
#### registerResult
216+
217+
This is the result from the auth server
218+
219+
* **clientId** - (`string`) the assigned client id
220+
* **clientIdIssuedAt** - (`string`) _OPTIONAL_ date string of when the client id was issued
221+
* **clientSecret** - (`string`) _OPTIONAL_ the assigned client secret
222+
* **clientSecretExpiresAt** - (`string`) date string of when the client secret expires, which will be provided if `clientSecret` is provided. If `new Date(clientSecretExpiresAt).getTime() === 0`, then the secret never expires
223+
* **registrationClientUri** - (`string`) _OPTIONAL_ uri that can be used to perform subsequent operations on the registration
224+
* **registrationAccessToken** - (`string`) token that can be used at the endpoint given by `registrationClientUri` to perform subsequent operations on the registration. Will be provided if `registrationClientUri` is provided
225+
182226
## Getting started
183227

184228
```sh

index.spec.js

Lines changed: 247 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import { authorize, refresh } from './';
1+
import { register, authorize, refresh } from './';
22

33
jest.mock('react-native', () => ({
44
NativeModules: {
55
RNAppAuth: {
6+
register: jest.fn(),
67
authorize: jest.fn(),
78
refresh: jest.fn(),
89
},
@@ -13,10 +14,14 @@ jest.mock('react-native', () => ({
1314
}));
1415

1516
describe('AppAuth', () => {
17+
let mockRegister;
1618
let mockAuthorize;
1719
let mockRefresh;
1820

1921
beforeAll(() => {
22+
mockRegister = require('react-native').NativeModules.RNAppAuth.register;
23+
mockRegister.mockReturnValue('REGISTERED');
24+
2025
mockAuthorize = require('react-native').NativeModules.RNAppAuth.authorize;
2126
mockAuthorize.mockReturnValue('AUTHORIZED');
2227

@@ -38,8 +43,242 @@ describe('AppAuth', () => {
3843
customHeaders: null,
3944
};
4045

46+
const registerConfig = {
47+
issuer: 'test-issuer',
48+
redirectUrls: ['test-redirectUrl'],
49+
responseTypes: ['code'],
50+
grantTypes: ['authorization_code'],
51+
subjectType: 'public',
52+
tokenEndpointAuthMethod: 'client_secret_post',
53+
additionalParameters: {},
54+
serviceConfiguration: null,
55+
};
56+
57+
describe('register', () => {
58+
beforeEach(() => {
59+
mockRegister.mockReset();
60+
mockAuthorize.mockReset();
61+
mockRefresh.mockReset();
62+
});
63+
64+
it('throws an error when issuer is not a string and serviceConfiguration is not passed', () => {
65+
expect(() => {
66+
register({ ...registerConfig, issuer: () => ({}) });
67+
}).toThrow('Config error: you must provide either an issuer or a registration endpoint');
68+
});
69+
70+
it('throws an error when serviceConfiguration does not have registrationEndpoint and issuer is not passed', () => {
71+
expect(() => {
72+
register({
73+
...registerConfig,
74+
issuer: undefined,
75+
serviceConfiguration: { authorizationEndpoint: '' },
76+
});
77+
}).toThrow('Config error: you must provide either an issuer or a registration endpoint');
78+
});
79+
80+
it('throws an error when redirectUrls is not an Array', () => {
81+
expect(() => {
82+
register({ ...registerConfig, redirectUrls: 'test-url' });
83+
}).toThrow('Config error: redirectUrls must be an Array of strings');
84+
});
85+
86+
it('throws an error when redirectUrls does not contain strings', () => {
87+
expect(() => {
88+
register({ ...registerConfig, redirectUrls: [null] });
89+
}).toThrow('Config error: redirectUrls must be an Array of strings');
90+
});
91+
92+
it('throws an error when responseTypes is not an Array', () => {
93+
expect(() => {
94+
register({ ...registerConfig, responseTypes: 'test-type' });
95+
}).toThrow('Config error: if provided, responseTypes must be an Array of strings');
96+
});
97+
98+
it('throws an error when responseTypes does not contain strings', () => {
99+
expect(() => {
100+
register({ ...registerConfig, responseTypes: [null] });
101+
}).toThrow('Config error: if provided, responseTypes must be an Array of strings');
102+
});
103+
104+
it('throws an error when grantTypes is not an Array', () => {
105+
expect(() => {
106+
register({ ...registerConfig, grantTypes: 'test-type' });
107+
}).toThrow('Config error: if provided, grantTypes must be an Array of strings');
108+
});
109+
110+
it('throws an error when grantTypes does not contain strings', () => {
111+
expect(() => {
112+
register({ ...registerConfig, grantTypes: [null] });
113+
}).toThrow('Config error: if provided, grantTypes must be an Array of strings');
114+
});
115+
116+
it('throws an error when subjectType is not a string', () => {
117+
expect(() => {
118+
register({ ...registerConfig, subjectType: 7 });
119+
}).toThrow('Config error: if provided, subjectType must be a string');
120+
});
121+
122+
it('throws an error when tokenEndpointAuthMethod is not a string', () => {
123+
expect(() => {
124+
register({ ...registerConfig, tokenEndpointAuthMethod: () => 'test-method' });
125+
}).toThrow('Config error: if provided, tokenEndpointAuthMethod must be a string');
126+
});
127+
128+
it('throws an error when customHeaders has too few keys', () => {
129+
expect(() => {
130+
register({ ...registerConfig, customHeaders: {} });
131+
}).toThrow();
132+
});
133+
134+
it('throws an error when customHeaders has too many keys', () => {
135+
expect(() => {
136+
register({
137+
...registerConfig,
138+
customHeaders: {
139+
register: { toto: 'titi' },
140+
authorize: { toto: 'titi' },
141+
unknownKey: { toto: 'titi' },
142+
},
143+
});
144+
}).toThrow();
145+
});
146+
147+
it('throws an error when customHeaders has unknown keys', () => {
148+
expect(() => {
149+
register({
150+
...registerConfig,
151+
customHeaders: {
152+
reg: { toto: 'titi' },
153+
authorize: { toto: 'titi' },
154+
},
155+
});
156+
}).toThrow();
157+
expect(() => {
158+
register({
159+
...registerConfig,
160+
customHeaders: {
161+
reg: { toto: 'titi' },
162+
},
163+
});
164+
}).toThrow();
165+
});
166+
167+
it('throws an error when customHeaders values arent Record<string,string>', () => {
168+
expect(() => {
169+
register({
170+
...registerConfig,
171+
customHeaders: {
172+
register: { toto: {} },
173+
},
174+
});
175+
}).toThrow();
176+
});
177+
178+
it('calls the native wrapper with the correct args on iOS', () => {
179+
register(registerConfig);
180+
expect(mockRegister).toHaveBeenCalledWith(
181+
registerConfig.issuer,
182+
registerConfig.redirectUrls,
183+
registerConfig.responseTypes,
184+
registerConfig.grantTypes,
185+
registerConfig.subjectType,
186+
registerConfig.tokenEndpointAuthMethod,
187+
registerConfig.additionalParameters,
188+
registerConfig.serviceConfiguration
189+
);
190+
});
191+
192+
describe('Android-specific', () => {
193+
beforeEach(() => {
194+
require('react-native').Platform.OS = 'android';
195+
});
196+
197+
afterEach(() => {
198+
require('react-native').Platform.OS = 'ios';
199+
});
200+
201+
describe('dangerouslyAllowInsecureHttpRequests parameter', () => {
202+
it('calls the native wrapper with default value `false`', () => {
203+
register(registerConfig);
204+
expect(mockRegister).toHaveBeenCalledWith(
205+
registerConfig.issuer,
206+
registerConfig.redirectUrls,
207+
registerConfig.responseTypes,
208+
registerConfig.grantTypes,
209+
registerConfig.subjectType,
210+
registerConfig.tokenEndpointAuthMethod,
211+
registerConfig.additionalParameters,
212+
registerConfig.serviceConfiguration,
213+
false,
214+
registerConfig.customHeaders
215+
);
216+
});
217+
218+
it('calls the native wrapper with passed value `false`', () => {
219+
register({ ...registerConfig, dangerouslyAllowInsecureHttpRequests: false });
220+
expect(mockRegister).toHaveBeenCalledWith(
221+
registerConfig.issuer,
222+
registerConfig.redirectUrls,
223+
registerConfig.responseTypes,
224+
registerConfig.grantTypes,
225+
registerConfig.subjectType,
226+
registerConfig.tokenEndpointAuthMethod,
227+
registerConfig.additionalParameters,
228+
registerConfig.serviceConfiguration,
229+
false,
230+
registerConfig.customHeaders
231+
);
232+
});
233+
234+
it('calls the native wrapper with passed value `true`', () => {
235+
register({ ...registerConfig, dangerouslyAllowInsecureHttpRequests: true });
236+
expect(mockRegister).toHaveBeenCalledWith(
237+
registerConfig.issuer,
238+
registerConfig.redirectUrls,
239+
registerConfig.responseTypes,
240+
registerConfig.grantTypes,
241+
registerConfig.subjectType,
242+
registerConfig.tokenEndpointAuthMethod,
243+
registerConfig.additionalParameters,
244+
registerConfig.serviceConfiguration,
245+
true,
246+
registerConfig.customHeaders
247+
);
248+
});
249+
});
250+
251+
describe('customHeaders parameter', () => {
252+
it('calls the native wrapper with headers', () => {
253+
const customTokenHeaders = { Authorization: 'Basic someBase64Value' };
254+
const customAuthorizeHeaders = { Authorization: 'Basic someOtherBase64Value' };
255+
const customRegisterHeaders = { Authorization: 'Basic some3rdBase64Value' };
256+
const customHeaders = {
257+
token: customTokenHeaders,
258+
authorize: customAuthorizeHeaders,
259+
register: customRegisterHeaders,
260+
};
261+
register({ ...registerConfig, customHeaders });
262+
expect(mockRegister).toHaveBeenCalledWith(
263+
registerConfig.issuer,
264+
registerConfig.redirectUrls,
265+
registerConfig.responseTypes,
266+
registerConfig.grantTypes,
267+
registerConfig.subjectType,
268+
registerConfig.tokenEndpointAuthMethod,
269+
registerConfig.additionalParameters,
270+
registerConfig.serviceConfiguration,
271+
false,
272+
customHeaders
273+
);
274+
});
275+
});
276+
});
277+
});
278+
41279
describe('authorize', () => {
42280
beforeEach(() => {
281+
mockRegister.mockReset();
43282
mockAuthorize.mockReset();
44283
mockRefresh.mockReset();
45284
});
@@ -210,7 +449,12 @@ describe('AppAuth', () => {
210449
it('calls the native wrapper with headers', () => {
211450
const customTokenHeaders = { Authorization: 'Basic someBase64Value' };
212451
const customAuthorizeHeaders = { Authorization: 'Basic someOtherBase64Value' };
213-
const customHeaders = { token: customTokenHeaders, authorize: customAuthorizeHeaders };
452+
const customRegisterHeaders = { Authorization: 'Basic some3rdBase64Value' };
453+
const customHeaders = {
454+
token: customTokenHeaders,
455+
authorize: customAuthorizeHeaders,
456+
register: customRegisterHeaders,
457+
};
214458
authorize({ ...config, customHeaders });
215459
expect(mockAuthorize).toHaveBeenCalledWith(
216460
config.issuer,
@@ -232,6 +476,7 @@ describe('AppAuth', () => {
232476

233477
describe('refresh', () => {
234478
beforeEach(() => {
479+
mockRegister.mockReset();
235480
mockAuthorize.mockReset();
236481
mockRefresh.mockReset();
237482
});

0 commit comments

Comments
 (0)