',
9 | redirectUrl: 'com.myapp://redirect/url/', // the redirectUrl must end with a slash
10 | scopes: ['openid', 'offline_access'],
11 | };
12 |
13 | // Log in to get an authentication token
14 | const authState = await authorize(config);
15 |
16 | // Refresh token
17 | const refreshedState = await refresh(config, {
18 | refreshToken: authState.refreshToken,
19 | });
20 | ```
21 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/Bug_Report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: ⚠️ Bug/Issue report
3 | about: Please provide as much detail as possible to help us with a bug or issue.
4 | ---
5 |
6 | ## Issue
7 |
8 |
9 |
10 | ---
11 |
12 | ## Environment
13 |
14 | * **Your Identity Provider**: `e.g. IdentityServer 4 / Okta / Azure`
15 | * **Platform that you're experiencing the issue on**: `iOS / Android / both`
16 | * **Your `react-native` Version**: `e.g. 0.72.1`
17 | * **Your `react-native-app-auth` Version**: `e.g. 7.0.0`
18 | * **Are you using Expo?**
19 |
--------------------------------------------------------------------------------
/docs/docs/usage/errors.md:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar_position: 7
3 | ---
4 |
5 | # Error Messages
6 |
7 | Values are in the `code` field of the rejected Error object.
8 |
9 | - OAuth Authorization [error codes](https://tools.ietf.org/html/rfc6749#section-4.1.2.1)
10 | - OAuth Access Token [error codes](https://tools.ietf.org/html/rfc6749#section-5.2)
11 | - OpendID Connect Registration [error codes](https://openid.net/specs/openid-connect-registration-1_0.html#RegistrationError)
12 | - `service_configuration_fetch_error` - could not fetch the service configuration
13 | - `authentication_failed` - user authentication failed
14 | - `token_refresh_failed` - could not exchange the refresh token for a new JWT
15 | - `registration_failed` - could not register
16 | - `browser_not_found` (Android only) - no suitable browser installed
17 |
--------------------------------------------------------------------------------
/examples/demo/android/app/src/release/java/com/example/ReactNativeFlipper.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) Meta Platforms, Inc. and affiliates.
3 | *
4 | * This source code is licensed under the MIT license found in the LICENSE file in the root
5 | * directory of this source tree.
6 | */
7 | package com.example;
8 |
9 | import android.content.Context;
10 | import com.facebook.react.ReactInstanceManager;
11 |
12 | /**
13 | * Class responsible of loading Flipper inside your React Native application. This is the release
14 | * flavor of it so it's empty as we don't want to load Flipper.
15 | */
16 | public class ReactNativeFlipper {
17 | public static void initializeFlipper(Context context, ReactInstanceManager reactInstanceManager) {
18 | // Do nothing as we don't want to initialize Flipper on Release.
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/examples/demo/ios/ExampleTests/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | BNDL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | 1
23 |
24 |
25 |
--------------------------------------------------------------------------------
/docs/docs/providers/keycloak.md:
--------------------------------------------------------------------------------
1 | # Keycloak
2 |
3 | Keycloak versions [prior to May 2020](https://github.com/keycloak/keycloak/pull/7106) do not specify a revocation endpoint so revoke functionality doesn't work. If you require the ability to call `revoke` you'll need to ensure you're on a modern version of Keycloak.
4 |
5 | If you use [JHipster](http://www.jhipster.tech/)'s default Keycloak Docker image, everything will work with the following settings.
6 |
7 | ```js
8 | const config = {
9 | issuer: 'http://localhost:9080/auth/realms/jhipster',
10 | clientId: 'web_app',
11 | redirectUrl: ':/callback',
12 | scopes: ['openid', 'profile'],
13 | };
14 |
15 | // Log in to get an authentication token
16 | const authState = await authorize(config);
17 |
18 | // Refresh token
19 | const refreshedState = await refresh(config, {
20 | refreshToken: authState.refreshToken,
21 | });
22 | ```
23 |
--------------------------------------------------------------------------------
/docs/src/components/landing/nf-link-button.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | interface ButtonProps {
4 | text: string;
5 | link: string;
6 | screenReaderLabel?: string;
7 | }
8 |
9 | export const NFLinkButton = ({ text, link }: ButtonProps) => {
10 | return (
11 |
15 | {text}
16 |
17 | );
18 | };
19 |
--------------------------------------------------------------------------------
/docs/docs/providers/github.md:
--------------------------------------------------------------------------------
1 | # GitHub
2 |
3 | Go to [OAuth Apps](https://github.com/settings/developers) to create your app.
4 |
5 | For the Authorization callback URL, choose something like `com.myapp://oauthredirect` and ensure you use `com.myapp` in your `appAuthRedirectScheme` in `android/app/build.gradle`.
6 |
7 | ```js
8 | const config = {
9 | redirectUrl: 'com.my.auth.app://oauthredirect',
10 | clientId: '',
11 | clientSecret: '',
12 | scopes: ['identity'],
13 | additionalHeaders: { Accept: 'application/json' },
14 | serviceConfiguration: {
15 | authorizationEndpoint: 'https://github.com/login/oauth/authorize',
16 | tokenEndpoint: 'https://github.com/login/oauth/access_token',
17 | revocationEndpoint: 'https://github.com/settings/connections/applications/',
18 | },
19 | };
20 |
21 | // Log in to get an authentication token
22 | const authState = await authorize(config);
23 | ```
24 |
--------------------------------------------------------------------------------
/packages/react-native-app-auth/android/src/main/java/com/rnappauth/utils/EndSessionResponseFactory.java:
--------------------------------------------------------------------------------
1 | package com.rnappauth.utils;
2 |
3 | import com.facebook.react.bridge.Arguments;
4 | import com.facebook.react.bridge.WritableMap;
5 |
6 | import net.openid.appauth.EndSessionResponse;
7 |
8 | public final class EndSessionResponseFactory {
9 | /*
10 | * Read raw end session response into a React Native map to be passed down the bridge
11 | */
12 | public static final WritableMap endSessionResponseToMap(EndSessionResponse response) {
13 | WritableMap map = Arguments.createMap();
14 |
15 | map.putString("state", response.state);
16 | map.putString("idTokenHint", response.request.idTokenHint);
17 | if (response.request.postLogoutRedirectUri != null) {
18 | map.putString("postLogoutRedirectUri", response.request.postLogoutRedirectUri.toString());
19 | }
20 |
21 | return map;
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/docs/docs/providers/slack.md:
--------------------------------------------------------------------------------
1 | # Slack
2 |
3 | If you don't already have a slack app, create it [here](https://api.slack.com/apps).
4 |
5 | Once you have an app, go "Add features and functionality" => "Permissions". Here you'll need to add two things:
6 |
7 | 1. Redirect URL
8 | Under "Redirect URLs", add one for your app, e.g. `com.myapp://oauth` and save
9 |
10 | 2. Scopes
11 | Under "Scopes", add the scopes you want to request from the user, e.g, "emoji:read"
12 |
13 | ```js
14 | const config = {
15 | clientId: '', // found under App Credentials
16 | clientSecret: '', // found under App Credentials
17 | scopes: ['emoji:read'], // choose any of the scopes set up in step 1
18 | redirectUrl: 'com.myapp://oauth', // set up in step 2
19 | serviceConfiguration: {
20 | authorizationEndpoint: 'https://slack.com/oauth/authorize',
21 | tokenEndpoint: 'https://slack.com/api/oauth.access',
22 | },
23 | };
24 |
25 | const authState = await authorize(config);
26 | ```
27 |
--------------------------------------------------------------------------------
/docs/docs/client-secrets.md:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar_position: 2
3 | ---
4 |
5 | # Client Secrets
6 |
7 | Some authentication providers, including examples cited below, require you to provide a client secret. The authors of the AppAuth library
8 |
9 | > [strongly recommend](https://github.com/openid/AppAuth-Android#utilizing-client-secrets-dangerous) you avoid using static client secrets in your native applications whenever possible. Client secrets derived via a dynamic client registration are safe to use, but static client secrets can be easily extracted from your apps and allow others to impersonate your app and steal user data. If client secrets must be used by the OAuth2 provider you are integrating with, we strongly recommend performing the code exchange step on your backend, where the client secret can be kept hidden.
10 |
11 | Having said this, in some cases using client secrets is unavoidable. In these cases, a `clientSecret` parameter can be provided to `authorize`/`refresh` calls when performing a token request.
12 |
--------------------------------------------------------------------------------
/docs/docs/providers/unsplash.md:
--------------------------------------------------------------------------------
1 | # Unsplash
2 |
3 | If you don't already have a unsplash app, create it [here](https://unsplash.com/oauth/applications).
4 |
5 | Once you have an app, go to your app page. Here you'll need to add two things:
6 |
7 | 1. Redirect URL
8 | Under "Redirect URLs", add one for your app, e.g. `com.myapp://oauth` and save
9 |
10 | 2. Scopes
11 | Under "Scopes", add the scopes you want to request from the user, e.g, "public"
12 |
13 | ```js
14 | const config = {
15 | usePKCE: false, // Important !!
16 | clientId: '', // found under App Credentials
17 | clientSecret: '', // found under App Credentials
18 | scopes: ['public'], // choose any of the scopes set up in step 1
19 | redirectUrl: 'com.myapp://oauth', // set up in step 2
20 | serviceConfiguration: {
21 | authorizationEndpoint: 'https://unsplash.com/oauth/authorize',
22 | tokenEndpoint: 'https://unsplash.com/oauth/token',
23 | },
24 | };
25 |
26 | const authState = await authorize(config);
27 | ```
28 |
--------------------------------------------------------------------------------
/packages/react-native-app-auth/android/src/main/java/com/rnappauth/RNAppAuthPackage.java:
--------------------------------------------------------------------------------
1 | package com.rnappauth;
2 |
3 | import java.util.Arrays;
4 | import java.util.Collections;
5 | import java.util.List;
6 |
7 | import com.facebook.react.ReactPackage;
8 | import com.facebook.react.bridge.NativeModule;
9 | import com.facebook.react.bridge.ReactApplicationContext;
10 | import com.facebook.react.uimanager.ViewManager;
11 | import com.facebook.react.bridge.JavaScriptModule;
12 |
13 | public class RNAppAuthPackage implements ReactPackage {
14 | @Override
15 | public List createNativeModules(ReactApplicationContext reactContext) {
16 | return Arrays.asList(new RNAppAuthModule(reactContext));
17 | }
18 |
19 | // Deprecated from RN 0.47
20 | public List> createJSModules() {
21 | return Collections.emptyList();
22 | }
23 |
24 | @Override
25 | public List createViewManagers(ReactApplicationContext reactContext) {
26 | return Collections.emptyList();
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/packages/react-native-app-auth/android/src/main/java/com/rnappauth/utils/MutableBrowserAllowList.java:
--------------------------------------------------------------------------------
1 | package com.rnappauth.utils;
2 |
3 | import androidx.annotation.NonNull;
4 |
5 | import net.openid.appauth.browser.BrowserDescriptor;
6 | import net.openid.appauth.browser.BrowserMatcher;
7 |
8 | import java.util.ArrayList;
9 | import java.util.List;
10 |
11 | public class MutableBrowserAllowList implements BrowserMatcher {
12 |
13 | private final List mBrowserMatchers = new ArrayList<>();
14 |
15 | public void add(BrowserMatcher browserMatcher) {
16 | mBrowserMatchers.add(browserMatcher);
17 | }
18 |
19 | public void remove(BrowserMatcher browserMatcher) {
20 | mBrowserMatchers.remove(browserMatcher);
21 | }
22 |
23 | @Override
24 | public boolean matches(@NonNull BrowserDescriptor descriptor) {
25 | for (BrowserMatcher matcher : mBrowserMatchers) {
26 | if (matcher.matches(descriptor)) {
27 | return true;
28 | }
29 | }
30 |
31 | return false;
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 | on:
3 | push:
4 | branches:
5 | - main
6 |
7 | jobs:
8 | release:
9 | runs-on: ubuntu-latest
10 | permissions:
11 | contents: write
12 | id-token: write
13 | issues: write
14 | repository-projects: write
15 | deployments: write
16 | packages: write
17 | pull-requests: write
18 |
19 | steps:
20 | - uses: actions/checkout@v4
21 |
22 | - name: Use Node.js
23 | uses: actions/setup-node@v4
24 | with:
25 | node-version: 18
26 | cache: 'yarn'
27 |
28 | - name: Install dependencies
29 | run: yarn install --frozen-lockfile
30 |
31 | - name: Unit Tests
32 | run: yarn test
33 |
34 | - name: PR or Publish
35 | id: changesets
36 | uses: changesets/action@v1
37 | with:
38 | version: yarn changeset version
39 | publish: yarn changeset publish
40 | env:
41 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
42 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
43 |
--------------------------------------------------------------------------------
/docs/config-examples/imgur.md:
--------------------------------------------------------------------------------
1 | # Imgur
2 |
3 | Imgur provides an OAuth 2.0 endpoint for logging in with a Imgur user's credentials. You'll need to first [register your Imgur application here](https://api.imgur.com/oauth2/addclient). See [this comment](https://github.com/FormidableLabs/react-native-app-auth/issues/516#issuecomment-2115465572) for detailed setup guide.
4 |
5 | Please note:
6 |
7 | * Imgur does not provide a OIDC discovery endpoint, so `serviceConfiguration` is used instead.
8 |
9 | ```js
10 | // your configuration should look something like this
11 | const config = {
12 | issuer: 'https://api.imgur.com/oauth2/',
13 | clientId: 'abc79a5abcdb30e', // your client id
14 | redirectUrl: encodeURIComponent('com.myapp://oauth/callback'), // must wrap it in encodeURIComponent
15 | scopes: [],
16 | serviceConfiguration: {
17 | authorizationEndpoint: 'https://api.imgur.com/oauth2/authorize',
18 | tokenEndpoint: 'https://api.imgur.com/oauth2/token',
19 | },
20 | };
21 |
22 | // Log in to get an authentication token
23 | const authState = await authorize(config);
24 | ```
25 |
--------------------------------------------------------------------------------
/examples/demo/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "rnaa-demo",
3 | "version": "1.0.1",
4 | "private": true,
5 | "scripts": {
6 | "android": "react-native run-android",
7 | "ios": "react-native run-ios",
8 | "lint": "eslint .",
9 | "start": "react-native start",
10 | "test": "jest"
11 | },
12 | "dependencies": {
13 | "react": "18.2.0",
14 | "react-native": "0.72.4",
15 | "react-native-app-auth": "*"
16 | },
17 | "devDependencies": {
18 | "@babel/core": "^7.20.0",
19 | "@babel/preset-env": "^7.20.0",
20 | "@babel/runtime": "^7.20.0",
21 | "@react-native/eslint-config": "^0.72.2",
22 | "@react-native/metro-config": "^0.72.11",
23 | "@tsconfig/react-native": "^3.0.0",
24 | "@types/react": "^18.0.24",
25 | "@types/react-test-renderer": "^18.0.0",
26 | "babel-jest": "^29.2.1",
27 | "eslint": "^8.19.0",
28 | "jest": "^29.2.1",
29 | "metro-react-native-babel-preset": "0.76.8",
30 | "prettier": "^2.4.1",
31 | "react-test-renderer": "18.2.0",
32 | "typescript": "4.8.4"
33 | },
34 | "engines": {
35 | "node": ">=16"
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2017 Formidable Labs
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of
6 | this software and associated documentation files (the "Software"), to deal in
7 | the Software without restriction, including without limitation the rights to
8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9 | the Software, and to permit persons to whom the Software is furnished to do so,
10 | subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/examples/demo/ios/Example/Images.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "iphone",
5 | "scale" : "2x",
6 | "size" : "20x20"
7 | },
8 | {
9 | "idiom" : "iphone",
10 | "scale" : "3x",
11 | "size" : "20x20"
12 | },
13 | {
14 | "idiom" : "iphone",
15 | "scale" : "2x",
16 | "size" : "29x29"
17 | },
18 | {
19 | "idiom" : "iphone",
20 | "scale" : "3x",
21 | "size" : "29x29"
22 | },
23 | {
24 | "idiom" : "iphone",
25 | "scale" : "2x",
26 | "size" : "40x40"
27 | },
28 | {
29 | "idiom" : "iphone",
30 | "scale" : "3x",
31 | "size" : "40x40"
32 | },
33 | {
34 | "idiom" : "iphone",
35 | "scale" : "2x",
36 | "size" : "60x60"
37 | },
38 | {
39 | "idiom" : "iphone",
40 | "scale" : "3x",
41 | "size" : "60x60"
42 | },
43 | {
44 | "idiom" : "ios-marketing",
45 | "scale" : "1x",
46 | "size" : "1024x1024"
47 | }
48 | ],
49 | "info" : {
50 | "author" : "xcode",
51 | "version" : 1
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/docs/docs/providers/microsoft.md:
--------------------------------------------------------------------------------
1 | # Microsoft
2 |
3 | 1. Supplying "issuer" fails, because Microsoft returns `issuer` with the literal string `https://login.microsoftonline.com/{tenantid}/v2.0` when `https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration` is queried.. We need to manually specify `serviceConfiguration`.
4 |
5 | 2. `REDIRECT_URL` varies based on platform:
6 |
7 | - iOS: msauth.com.example.app://auth/
8 | - Android: com.example.app://msauth/``/
9 |
10 | 3. Microsoft does not have. revocationEndpoint.
11 |
12 | ```js
13 | const config = {
14 | serviceConfiguration: {
15 | authorizationEndpoint: 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize',
16 | tokenEndpoint: 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
17 | },
18 | clientId: '',
19 | redirectUrl: '',
20 | scopes: ['openid', 'profile', 'email', 'offline_access'],
21 | };
22 |
23 | // Log in to get an authentication token
24 | const authState = await authorize(config);
25 |
26 | // Refresh token
27 | const refreshedState = await refresh(config, {
28 | refreshToken: authState.refreshToken,
29 | });
30 | ```
31 |
--------------------------------------------------------------------------------
/docs/docs/providers/reddit.md:
--------------------------------------------------------------------------------
1 | # Reddit
2 |
3 | Log in and go to [apps](https://www.reddit.com/prefs/apps) to create your app.
4 |
5 | Choose "installed app" and give it a name, description and about url of your choosing.
6 |
7 | For the redirect uri, choose something like `com.myapp//oauth2redirect/reddit` and make sure that you use `com.myapp` in your `appAuthRedirectScheme` in `android/app/build.gradle`.
8 |
9 | Reddit requires for you to add a [basic auth header](https://github.com/reddit-archive/reddit/wiki/oauth2#retrieving-the-access-token) to the token request.
10 |
11 | ```js
12 | const config = {
13 | redirectUrl: 'com.myapp://oauth2redirect/reddit',
14 | clientId: '',
15 | clientSecret: '', // empty string - needed for iOS
16 | scopes: ['identity'],
17 | serviceConfiguration: {
18 | authorizationEndpoint: 'https://www.reddit.com/api/v1/authorize.compact',
19 | tokenEndpoint: 'https://www.reddit.com/api/v1/access_token',
20 | },
21 | customHeaders: {
22 | token: {
23 | Authorization: 'Basic ',
24 | },
25 | },
26 | };
27 |
28 | // Log in to get an authentication token
29 | const authState = await authorize(config);
30 | ```
31 |
--------------------------------------------------------------------------------
/docs/docs/providers/fusionauth.md:
--------------------------------------------------------------------------------
1 | # FusionAuth
2 |
3 | FusionAuth does not specify a revocation endpoint so revoke functionality doesn't work. Other than that, full functionality is available.
4 |
5 | - [Install FusionAuth](https://fusionauth.io/docs/v1/tech/installation-guide).
6 | - Create an application in the admin screen. Note the client id.
7 | - Set the redirect_uri for the application to be a value like `fusionauth.demo:/oauthredirect` where `fusionauth.demo` is a scheme you've registered in your application.
8 |
9 | Use the following configuration (replacing the `clientId` with your application id and `fusionAuth.demo` with your scheme):
10 |
11 | ```js
12 | const config = {
13 | issuer: 'http://localhost:9011',
14 | clientId: '253eb7aa-687a-4bf3-b12b-26baa40eecbf',
15 | redirectUrl: 'fusionauth.demo:/callback',
16 | scopes: ['offline_access', 'openid'],
17 | };
18 |
19 | // Log in to get an authentication token
20 | const authState = await authorize(config);
21 |
22 | // Refresh token
23 | const refreshedState = await refresh(config, {
24 | refreshToken: authState.refreshToken,
25 | });
26 | ```
27 |
28 | Check out a full tutorial here: https://fusionauth.io/blog/2020/08/19/securing-react-native-with-oauth
29 |
--------------------------------------------------------------------------------
/examples/demo/metro.config.js:
--------------------------------------------------------------------------------
1 | const {getDefaultConfig, mergeConfig} = require('@react-native/metro-config');
2 |
3 | const path = require('path');
4 |
5 | const packagePath = path.resolve(
6 | path.join(process.cwd(), '..', '..', 'packages', 'react-native-app-auth'),
7 | );
8 |
9 | const projectRoot = __dirname;
10 | const monorepoRoot = path.resolve(projectRoot, '../..');
11 |
12 | const extraNodeModules = {
13 | 'react-native-app-auth': packagePath,
14 | };
15 | const watchFolders = [monorepoRoot, packagePath];
16 |
17 | /**
18 | * Metro configuration
19 | * https://facebook.github.io/metro/docs/configuration
20 | *
21 | * @type {import('metro-config').MetroConfig}
22 | */
23 | const config = {
24 | resolver: {
25 | extraNodeModules: new Proxy(extraNodeModules, {
26 | get: (target, name) =>
27 | name in target
28 | ? target[name]
29 | : path.join(process.cwd(), '..', '..', 'node_modules', name),
30 | }),
31 | unstable_enableSymlinks: true,
32 | },
33 | watchFolders,
34 | };
35 |
36 | config.resolver.nodeModulesPaths = [
37 | path.resolve(projectRoot, 'node_modules'),
38 | path.resolve(monorepoRoot, 'node_modules'),
39 | ];
40 |
41 | module.exports = mergeConfig(getDefaultConfig(__dirname), config);
42 |
--------------------------------------------------------------------------------
/docs/docs/providers/coinbase.md:
--------------------------------------------------------------------------------
1 | # Coinbase
2 |
3 | Create a new OAuth application in the [console](https://www.coinbase.com/oauth/applications/new).
4 |
5 | After the application is created, note down the clientId and secret (the secret will only be shown once. If you forgot to take note of it, you'll have to recreate your OAuth application).
6 |
7 | ```js
8 | const config = {
9 | clientId: '',
10 | clientSecret: '',
11 | redirectUrl: 'myapp://redirect', // this can be any valid uri as long as it's the same as what you configured
12 | scopes: ['wallet:accounts:read'], // https://developers.coinbase.com/docs/wallet/permissions
13 | serviceConfiguration: {
14 | authorizationEndpoint: 'https://www.coinbase.com/oauth/authorize',
15 | tokenEndpoint: 'https://api.coinbase.com/oauth/token',
16 | revocationEndpoint: 'https://api.coinbase.com/oauth/revoke',
17 | },
18 | };
19 |
20 | // Log in to get an authentication token
21 | const authState = await authorize(config);
22 |
23 | // Refresh token
24 | const refreshedState = await refresh(config, {
25 | refreshToken: authState.refreshToken,
26 | });
27 |
28 | // Revoke token
29 | await revoke(config, {
30 | tokenToRevoke: refreshedState.refreshToken,
31 | });
32 | ```
33 |
--------------------------------------------------------------------------------
/docs/docs/providers/asgardeo.md:
--------------------------------------------------------------------------------
1 | # Asgardeo
2 |
3 | To add authentication to your app using Asgardeo, you will first need to [create an application](https://wso2.com/asgardeo/docs/guides/applications/register-mobile-app/) in the Asgardeo console. If you don't have an Asgardeo account, [you can signup for one free](https://asgardeo.io/signup).
4 |
5 | After creating an application, take note of the configuration values listed in the **Quick Start** and **Info** tabs. You will be using those values as follows.
6 |
7 | ```js
8 | export const config = {
9 | issuer: 'https://api.asgardeo.io/t//oauth2/token',
10 | clientId: '',
11 | redirectUrl: '://example',
12 | scopes: ['openid', 'profile'],
13 | };
14 |
15 | // Log in to get an authentication token
16 | const authState = await authorize(config);
17 |
18 | // Refresh token
19 | const refreshedState = await refresh(config, {
20 | refreshToken: authState.refreshToken,
21 | });
22 |
23 | // Revoke token
24 | await revoke(config, {
25 | tokenToRevoke: refreshedState.refreshToken,
26 | });
27 |
28 | // End session
29 | await logout(config, {
30 | idToken: authState.idToken,
31 | postLogoutRedirectUrl: ':/logout',
32 | });
33 | ```
34 |
--------------------------------------------------------------------------------
/examples/demo/android/app/src/main/java/com/example/MainActivity.java:
--------------------------------------------------------------------------------
1 | package com.example;
2 |
3 | import com.facebook.react.ReactActivity;
4 | import com.facebook.react.ReactActivityDelegate;
5 | import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint;
6 | import com.facebook.react.defaults.DefaultReactActivityDelegate;
7 |
8 | public class MainActivity extends ReactActivity {
9 |
10 | /**
11 | * Returns the name of the main component registered from JavaScript. This is used to schedule
12 | * rendering of the component.
13 | */
14 | @Override
15 | protected String getMainComponentName() {
16 | return "Example";
17 | }
18 |
19 | /**
20 | * Returns the instance of the {@link ReactActivityDelegate}. Here we use a util class {@link
21 | * DefaultReactActivityDelegate} which allows you to easily enable Fabric and Concurrent React
22 | * (aka React 18) with two boolean flags.
23 | */
24 | @Override
25 | protected ReactActivityDelegate createReactActivityDelegate() {
26 | return new DefaultReactActivityDelegate(
27 | this,
28 | getMainComponentName(),
29 | // If you opted-in for the New Architecture, we enable the Fabric Renderer.
30 | DefaultNewArchitectureEntryPoint.getFabricEnabled());
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/examples/demo/.gitignore:
--------------------------------------------------------------------------------
1 | # OSX
2 | #
3 | .DS_Store
4 |
5 | # Xcode
6 | #
7 | build/
8 | *.pbxuser
9 | !default.pbxuser
10 | *.mode1v3
11 | !default.mode1v3
12 | *.mode2v3
13 | !default.mode2v3
14 | *.perspectivev3
15 | !default.perspectivev3
16 | xcuserdata
17 | *.xccheckout
18 | *.moved-aside
19 | DerivedData
20 | *.hmap
21 | *.ipa
22 | *.xcuserstate
23 | ios/.xcode.env.local
24 |
25 | # Android/IntelliJ
26 | #
27 | build/
28 | .idea
29 | .gradle
30 | local.properties
31 | *.iml
32 | *.hprof
33 | .cxx/
34 | *.keystore
35 | !debug.keystore
36 |
37 | # node.js
38 | #
39 | node_modules/
40 | npm-debug.log
41 | yarn-error.log
42 |
43 | # fastlane
44 | #
45 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
46 | # screenshots whenever they are needed.
47 | # For more information about the recommended setup visit:
48 | # https://docs.fastlane.tools/best-practices/source-control/
49 |
50 | **/fastlane/report.xml
51 | **/fastlane/Preview.html
52 | **/fastlane/screenshots
53 | **/fastlane/test_output
54 |
55 | # Bundle artifact
56 | *.jsbundle
57 |
58 | # Ruby / CocoaPods
59 | /ios/Pods/
60 | /vendor/bundle/
61 |
62 | # Temporary files created by Metro to check the health of the file watcher
63 | .metro-health-check*
64 |
65 | # testing
66 | /coverage
67 |
--------------------------------------------------------------------------------
/docs/docs/providers/fitbit.md:
--------------------------------------------------------------------------------
1 | # Fitbit
2 |
3 | Fitbit provides an OAuth 2.0 endpoint for logging in with a Fitbit user's credentials. You'll need to first [register your Fitbit application here](https://dev.fitbit.com/apps/new).
4 |
5 | Please note:
6 |
7 | - Fitbit does not provide a OIDC discovery endpoint, so `serviceConfiguration` is used instead.
8 | - Fitbit OAuth requires a [client secret](/docs/client-secrets).
9 |
10 | ```js
11 | const config = {
12 | clientId: 'your-client-id-generated-by-fitbit',
13 | clientSecret: 'your-client-secret-generated-by-fitbit',
14 | redirectUrl: 'com.whatever.url.you.configured.in.fitbit.oauth://redirect', //note: path is required
15 | scopes: ['activity', 'sleep'],
16 | serviceConfiguration: {
17 | authorizationEndpoint: 'https://www.fitbit.com/oauth2/authorize',
18 | tokenEndpoint: 'https://api.fitbit.com/oauth2/token',
19 | revocationEndpoint: 'https://api.fitbit.com/oauth2/revoke',
20 | },
21 | };
22 |
23 | // Log in to get an authentication token
24 | const authState = await authorize(config);
25 |
26 | // Refresh token
27 | const refreshedState = await refresh(config, {
28 | refreshToken: authState.refreshToken,
29 | });
30 |
31 | // Revoke token
32 | await revoke(config, {
33 | tokenToRevoke: refreshedState.refreshToken,
34 | includeBasicAuth: true,
35 | });
36 | ```
37 |
--------------------------------------------------------------------------------
/docs/docs/providers/dropbox.md:
--------------------------------------------------------------------------------
1 | # Dropbox
2 |
3 | Dropbox provides an OAuth 2.0 endpoint for logging in with a Dropbox user's credentials. You'll need to first [register your Dropbox application here](https://www.dropbox.com/developers/apps/create).
4 |
5 | Please note:
6 |
7 | - Dropbox does not provide a OIDC discovery endpoint, so `serviceConfiguration` is used instead.
8 | - Dropbox OAuth requires a [client secret](/docs/client-secrets).
9 | - Dropbox access tokens are short lived and will expire after a short period of time. To update your access token a separate call needs to be made to [/oauth2/token](https://www.dropbox.com/developers/documentation/http/documentation#oauth2-token) to obtain a new access token.
10 |
11 | ```js
12 | const config = {
13 | clientId: 'your-client-id-generated-by-dropbox',
14 | clientSecret: 'your-client-secret-generated-by-dropbox',
15 | redirectUrl: 'your.app.bundle.id://oauth',
16 | scopes: [],
17 | serviceConfiguration: {
18 | authorizationEndpoint: 'https://www.dropbox.com/oauth2/authorize',
19 | tokenEndpoint: `https://www.dropbox.com/oauth2/token`,
20 | },
21 | additionalParameters: {
22 | token_access_type: 'offline',
23 | },
24 | };
25 |
26 | // Log in to get an authentication token
27 | const authState = await authorize(config);
28 | const dropboxUID = authState.tokenAdditionalParameters.account_id;
29 | ```
30 |
--------------------------------------------------------------------------------
/docs/docs/providers/uber.md:
--------------------------------------------------------------------------------
1 | # Uber
2 |
3 | Uber provides an OAuth 2.0 endpoint for logging in with a Uber user's credentials. You'll need to first [create an Uber OAuth application here](https://developer.uber.com/docs/riders/guides/authentication/introduction).
4 |
5 | Please note:
6 |
7 | - Uber does not provide a OIDC discovery endpoint, so `serviceConfiguration` is used instead.
8 | - Uber OAuth requires a [client secret](/docs/client-secrets).
9 |
10 | ```js
11 | const config = {
12 | clientId: 'your-client-id-generated-by-uber',
13 | clientSecret: 'your-client-secret-generated-by-uber',
14 | redirectUrl: 'com.whatever.url.you.configured.in.uber.oauth://redirect', //note: path is required
15 | scopes: ['profile', 'delivery'], // whatever scopes you configured in Uber OAuth portal
16 | serviceConfiguration: {
17 | authorizationEndpoint: 'https://login.uber.com/oauth/v2/authorize',
18 | tokenEndpoint: 'https://login.uber.com/oauth/v2/token',
19 | revocationEndpoint: 'https://login.uber.com/oauth/v2/revoke',
20 | },
21 | };
22 |
23 | // Log in to get an authentication token
24 | const authState = await authorize(config);
25 |
26 | // Refresh token
27 | const refreshedState = await refresh(config, {
28 | refreshToken: authState.refreshToken,
29 | });
30 |
31 | // Revoke token
32 | await revoke(config, {
33 | tokenToRevoke: refreshedState.refreshToken,
34 | });
35 | ```
36 |
--------------------------------------------------------------------------------
/examples/demo/README.md:
--------------------------------------------------------------------------------
1 | # React Native App Auth Example
2 |
3 | 
4 |
5 | ## Running the iOS app
6 |
7 | After cloning the repository, run the following:
8 |
9 | ```sh
10 | cd react-native-app-auth/Example
11 | yarn
12 | (cd ios && pod install)
13 | npx react-native run-ios
14 | ```
15 |
16 | ## Running the Android app
17 |
18 | After cloning the repository, run the following:
19 |
20 | ```sh
21 | cd react-native-app-auth/Example
22 | yarn
23 | npx react-native run-android
24 | ```
25 |
26 | ### Notes
27 | * You have to have the emulator open before running the last command. If you have difficulty getting the emulator to connect, open the project from Android Studio and run it through there.
28 | * ANDROID: When integrating with a project that utilizes deep linking (e.g. [React Navigation deep linking](https://reactnavigation.org/docs/deep-linking/#set-up-with-bare-react-native-projects)), update the redirectUrl in your config and the `appAuthRedirectScheme` value in build.gradle to use a custom scheme so that it differs from the scheme used in your deep linking intent-filter [as seen here](https://github.com/FormidableLabs/react-native-app-auth/issues/494#issuecomment-797394994).
29 |
30 | Example:
31 | ```
32 | // build.gradle
33 | android {
34 | defaultConfig {
35 | manifestPlaceholders = [
36 | appAuthRedirectScheme: 'io.identityserver.demo.auth'
37 | ]
38 | }
39 | }
40 | ```
41 |
--------------------------------------------------------------------------------
/packages/react-native-app-auth/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-native-app-auth",
3 | "version": "8.0.1",
4 | "description": "React Native bridge for AppAuth for supporting any OAuth 2 provider",
5 | "main": "index.js",
6 | "types": "index.d.ts",
7 | "scripts": {
8 | "test": "jest",
9 | "lint": "eslint ."
10 | },
11 | "files": [
12 | "android",
13 | "ios",
14 | "index.d.ts",
15 | "react-native-app-auth.podspec"
16 | ],
17 | "repository": {
18 | "type": "git",
19 | "url": "https://github.com/FormidableLabs/react-native-app-auth"
20 | },
21 | "bugs": {
22 | "url": "https://github.com/FormidableLabs/react-native-app-auth/issues"
23 | },
24 | "homepage": "https://github.com/FormidableLabs/react-native-app-auth",
25 | "keywords": [
26 | "react",
27 | "react-native",
28 | "auth",
29 | "authentication",
30 | "oauth",
31 | "oauth2",
32 | "appauth"
33 | ],
34 | "license": "MIT",
35 | "author": "Nearform Commerce (https://commerce.nearform.com)",
36 | "peerDependencies": {
37 | "react-native": ">=0.63.0"
38 | },
39 | "devDependencies": {
40 | "jest": "24.9.0",
41 | "react": "16.9.0",
42 | "react-native": "0.63.0"
43 | },
44 | "dependencies": {
45 | "invariant": "2.2.4",
46 | "react-native-base64": "0.0.2"
47 | },
48 | "jest": {
49 | "preset": "react-native"
50 | },
51 | "publishConfig": {
52 | "provenance": true
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/docs/docs/providers/identity-server-3.md:
--------------------------------------------------------------------------------
1 | # Identity Server 3
2 |
3 | This library supports authenticating with Identity Server 3. The only difference from
4 | Identity Server 4 is that it requires a `clientSecret` and there is no way to opt out of it.
5 |
6 | ```js
7 | // You must include a clientSecret
8 | const config = {
9 | issuer: 'your-identityserver-url',
10 | clientId: 'your-client-id',
11 | clientSecret: 'your-client-secret',
12 | redirectUrl: 'com.your.app.name:/oauthredirect',
13 | scopes: ['openid', 'profile', 'offline_access'],
14 | };
15 |
16 | // Log in to get an authentication token
17 | const authState = await authorize(config);
18 |
19 | // Refresh token
20 | const refreshedState = await refresh(config, {
21 | refreshToken: authState.refreshToken,
22 | });
23 |
24 | // Revoke token, note that Identity Server expects a client id on revoke
25 | await revoke(config, {
26 | tokenToRevoke: refreshedState.refreshToken,
27 | sendClientId: true,
28 | });
29 | ```
30 |
31 |
32 | Example server configuration
33 |
34 | ```
35 | var client = new Client
36 | {
37 | ClientId = "native.code",
38 | ClientName = "Native Client (Code with PKCE)",
39 | Flow = Flows.AuthorizationCodeWithProofKey,
40 | RedirectUris = { "com.your.app.name:/oauthredirect" },
41 | ClientSecrets = new List { new Secret("your-client-secret".Sha256()) },
42 | AllowAccessToAllScopes = true
43 | };
44 | ```
45 |
46 |
47 |
--------------------------------------------------------------------------------
/docs/src/components/landing/landing-features.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Divider } from './divider';
3 | import { NFLinkButton } from './nf-link-button';
4 |
5 | export const LandingFeatures = ({
6 | cta,
7 | heading,
8 | description,
9 | list,
10 | showDivider,
11 | }: {
12 | cta: { link: string; text: string };
13 | heading: string;
14 | description?: string;
15 | list: {
16 | imgSrc: string;
17 | alt: string;
18 | title: string;
19 | body?: string;
20 | html?: { __html: string };
21 | }[];
22 | showDivider?: boolean;
23 | }) => (
24 |
25 | {showDivider &&
}
26 |
{heading}
27 | {description &&
{description}
}
28 |
29 | {list.map(({ alt, body, imgSrc, title, html }, i) => (
30 | -
31 |
32 | {title}
33 |
34 | {body}
35 |
36 |
37 | ))}
38 |
39 |
40 |
41 | );
42 |
--------------------------------------------------------------------------------
/docs/docs/usage/authorization.md:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar_position: 2
3 | ---
4 |
5 | # Authorization
6 |
7 | This is the main function to use for authentication. Invoking this function will do the whole login
8 | flow and returns the access token, refresh token and access token expiry date when successful, or it
9 | throws an error when not successful.
10 |
11 | ```js
12 | import { authorize } from 'react-native-app-auth';
13 |
14 | const config = {
15 | issuer: '',
16 | clientId: '',
17 | redirectUrl: '',
18 | scopes: [''],
19 | };
20 |
21 | const result = await authorize(config);
22 | ```
23 |
24 | #### `result`
25 |
26 | This is the result from the auth server:
27 |
28 | - **accessToken** - (`string`) the access token
29 | - **accessTokenExpirationDate** - (`string`) the token expiration date
30 | - **authorizeAdditionalParameters** - (`Object`) additional url parameters from the authorizationEndpoint response.
31 | - **tokenAdditionalParameters** - (`Object`) additional url parameters from the tokenEndpoint response.
32 | - **idToken** - (`string`) the id token
33 | - **refreshToken** - (`string`) the refresh token
34 | - **tokenType** - (`string`) the token type, e.g. Bearer
35 | - **scopes** - ([`string`]) the scopes the user has agreed to be granted
36 | - **authorizationCode** - (`string`) the authorization code (only if `skipCodeExchange=true`)
37 | - **codeVerifier** - (`string`) the codeVerifier value used for the PKCE exchange (only if both `skipCodeExchange=true` and `usePKCE=true`)
38 |
--------------------------------------------------------------------------------
/docs/docs/providers/microsoft-entra-id.md:
--------------------------------------------------------------------------------
1 | # Microsoft Entra ID
2 |
3 | If you're using Microsoft Identity platform and want to add App Auth to your React Native application, you'll need an Entra application to authorize against.
4 |
5 | Microsoft offers mupltiple different PLatform configurations you could setup for your application. In this example, we are using the `Mobile and desktop applications` platform configuration.
6 |
7 | You can find detailed instructions on registering a new Entra application [here](https://learn.microsoft.com/en-us/entra/identity-platform/quickstart-register-app?tabs=certificate).
8 |
9 | NOTES:
10 |
11 | - Microsoft Entra ID does not have a `revocationEndpoint`.
12 | - Application ID can be viewed in your Entra application's dashboard.
13 | - Authorization and Token endpoints can be found under the `Endpoints` link at the top of the page in your Entra application's dashboard.
14 |
15 | ```js
16 | const config = {
17 | serviceConfiguration: {
18 | authorizationEndpoint: 'https://login.microsoftonline.com/{TENANT_ID}/oauth2/v2.0/authorize',
19 | tokenEndpoint: 'https://login.microsoftonline.com/{TENANT_ID}/oauth2/v2.0/token',
20 | },
21 | clientId: '',
22 | redirectUrl: 'com.my-app://oauth/redirect', // 'com.my-app' should correspond to your app name
23 | scopes: ['openid', 'profile', 'email', 'offline_access'],
24 | };
25 |
26 | // Log in to get an authentication token
27 | const authState = await authorize(config);
28 |
29 | // Refresh token
30 | const refreshedState = await refresh(config, {
31 | refreshToken: authState.refreshToken,
32 | });
33 | ```
34 |
--------------------------------------------------------------------------------
/docs/docs/providers/google.md:
--------------------------------------------------------------------------------
1 | # Google
2 |
3 | Full support out of the box.
4 |
5 | ```js
6 | const GOOGLE_OAUTH_APP_GUID = 'YOUR_GOOGLE_OAUTH_APP_GUID'; // it looks something like 12345678912-k50abcdefghijkabcdefghijkabcdefv
7 | const config = {
8 | issuer: 'https://accounts.google.com',
9 | clientId: `${GOOGLE_OAUTH_APP_GUID}.apps.googleusercontent.com`,
10 | redirectUrl: `com.googleusercontent.apps.${GOOGLE_OAUTH_APP_GUID}:/oauth2redirect/google`,
11 | scopes: ['openid', 'profile'],
12 | };
13 |
14 | // Log in to get an authentication token
15 | const authState = await authorize(config);
16 |
17 | // Refresh token
18 | const refreshedState = await refresh(config, {
19 | refreshToken: authState.refreshToken,
20 | });
21 |
22 | // Revoke token
23 | await revoke(config, {
24 | tokenToRevoke: refreshedState.refreshToken,
25 | });
26 | ```
27 |
28 | ### Note for Android
29 |
30 | To [capture the authorization redirect](https://github.com/openid/AppAuth-android#capturing-the-authorization-redirect), add the following property to the defaultConfig in `android/app/build.gradle`:
31 |
32 | ```
33 | android {
34 | defaultConfig {
35 | manifestPlaceholders = [
36 | appAuthRedirectScheme: 'com.googleusercontent.apps.YOUR_GOOGLE_OAUTH_APP_GUID'
37 | // your url will look like com.googleusercontent.apps.12345678912-k50abcdefghijkabcdefghijkabcdefv
38 | ]
39 | }
40 | }
41 | ```
42 |
43 | - You need to check custom URI scheme under APIs & Services -> Credentials -> OAuth 2.0 Client IDs -> Your Client Name -> Advanced Settings
44 | - It may take 5 minutes to a few hours for settings to take effect.
45 |
--------------------------------------------------------------------------------
/docs/docs/providers/identity-server-4.md:
--------------------------------------------------------------------------------
1 | # Identity Server 4
2 |
3 | This library supports authenticating for Identity Server 4 out of the box. Some quirks:
4 |
5 | 1. In order to enable refresh tokens, `offline_access` must be passed in as a scope variable
6 | 2. In order to revoke the access token, we must sent client id in the method body of the request.
7 | This is not part of the OAuth spec.
8 |
9 | ```js
10 | // Note "offline_access" scope is required to get a refresh token
11 | const config = {
12 | issuer: 'https://demo.identityserver.io',
13 | clientId: 'native.code',
14 | redirectUrl: 'io.identityserver.demo:/oauthredirect',
15 | scopes: ['openid', 'profile', 'offline_access'],
16 | };
17 |
18 | // Log in to get an authentication token
19 | const authState = await authorize(config);
20 |
21 | // Refresh token
22 | const refreshedState = await refresh(config, {
23 | refreshToken: authState.refreshToken,
24 | });
25 |
26 | // Revoke token, note that Identity Server expects a client id on revoke
27 | await revoke(config, {
28 | tokenToRevoke: refreshedState.refreshToken,
29 | sendClientId: true,
30 | });
31 | ```
32 |
33 |
34 | Example server configuration
35 |
36 | ```
37 | var client = new Client
38 | {
39 | ClientId = "native.code",
40 | ClientName = "Native Client (Code with PKCE)",
41 | RequireClientSecret = false,
42 | RedirectUris = { "io.identityserver.demo:/oauthredirect" },
43 | AllowedGrantTypes = GrantTypes.Code,
44 | RequirePkce = true,
45 | AllowedScopes = { "openid", "profile" },
46 | AllowOfflineAccess = true
47 | };
48 | ```
49 |
50 |
51 |
--------------------------------------------------------------------------------
/docs/docs/providers/strava.md:
--------------------------------------------------------------------------------
1 | # Strava
2 |
3 | Strava is not 100% spec compliant, but it is still possible to get this to work with this library.
4 |
5 | If you don't already have an app, create one [here](https://www.strava.com/settings/apps).
6 |
7 | Now add a redirect uri [here](https://www.strava.com/settings/api). Unlike most providers that ask you to define the entire callback uri, here you need to add the "Authorization Callback Domain", e.g. `oauthredirect` (it can be anything really, in this case the redirect uri in used in the config will be `com.myapp://oauthredirect`).
8 |
9 | Now go to the app page to find the client id and secret and use them in your config like so:
10 |
11 | ```js
12 | config = {
13 | clientId: '',
14 | clientSecret: '',
15 | redirectUrl: 'myapp://oauthredirect',
16 | serviceConfiguration: {
17 | authorizationEndpoint: 'https://www.strava.com/oauth/mobile/authorize',
18 | tokenEndpoint:
19 | 'https://www.strava.com/oauth/token?client_id=&client_secret=',
20 | },
21 | scopes: ['activity:read_all'],
22 | };
23 |
24 | const authState = await authorize(config);
25 | ```
26 |
27 | Note, they require the client secret and id being passed in the token endpoint. This is not in the spec and thus is not supported. But we can get around it by adding them to the tokenEndpoint as url params.
28 |
29 | ## Revocation
30 |
31 | The built in token revocation also won't work for Strava, because they use the param `access_token` instead of `token`, but you can easily implement it yourself using `fetch`
32 |
33 | ```js
34 | const res = await fetch(`https://www.strava.com/oauth/deauthorize?access_token=${accessToken}`, {
35 | method: 'POST',
36 | });
37 | ```
38 |
--------------------------------------------------------------------------------
/docs/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "rnaa-docs",
3 | "version": "0.0.0",
4 | "private": true,
5 | "scripts": {
6 | "docusaurus": "docusaurus",
7 | "start": "docusaurus start",
8 | "build": "docusaurus build --out-dir build/open-source/react-native-app-auth",
9 | "swizzle": "docusaurus swizzle",
10 | "deploy": "docusaurus deploy",
11 | "clear": "docusaurus clear",
12 | "serve": "docusaurus serve",
13 | "write-translations": "docusaurus write-translations",
14 | "write-heading-ids": "docusaurus write-heading-ids",
15 | "lint": "eslint --ext .js,.ts,.tsx src",
16 | "typecheck": "tsc"
17 | },
18 | "dependencies": {
19 | "@docusaurus/core": "3.3.2",
20 | "@docusaurus/plugin-google-gtag": "^3.5.2",
21 | "@docusaurus/preset-classic": "3.3.2",
22 | "@easyops-cn/docusaurus-search-local": "^0.41.0",
23 | "@mdx-js/react": "^3.0.0",
24 | "clsx": "^2.0.0",
25 | "formidable-oss-badges": "^1.4.1",
26 | "prism-react-renderer": "^2.3.0",
27 | "react": "^18.0.0",
28 | "react-dom": "^18.0.0",
29 | "react-native-app-auth": "^7.2.0"
30 | },
31 | "devDependencies": {
32 | "@docusaurus/module-type-aliases": "3.3.2",
33 | "@docusaurus/tsconfig": "3.3.2",
34 | "@docusaurus/types": "3.3.2",
35 | "autoprefixer": "^10.4.19",
36 | "postcss": "^8.4.38",
37 | "tailwindcss": "^3.4.3",
38 | "typescript": "~5.2.2"
39 | },
40 | "browserslist": {
41 | "production": [
42 | ">0.5%",
43 | "not dead",
44 | "not op_mini all"
45 | ],
46 | "development": [
47 | "last 3 chrome version",
48 | "last 3 firefox version",
49 | "last 5 safari version"
50 | ]
51 | },
52 | "engines": {
53 | "node": ">=18.0"
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/docs/docs/providers/aws-cognito.md:
--------------------------------------------------------------------------------
1 | # AWS Cognito
2 |
3 | First, set up a your user pool in [the AWS console](https://eu-west-1.console.aws.amazon.com/cognito). In the details of your new user pool, go down to `App clients` to create a new client. Make sure you create a client **without** a client secret (it's redundant on mobile). You should get an alphanumeric string which is your ``.
4 |
5 | Now you need to set up your domain name. This will be on the left menu in your pool details page, under App Integration -> Domain Name. What this is depends on your preference. E.g. for AppAuth demo, mine is `https://app-auth-test.auth.eu-west-1.amazoncognito.com` as I chose `app-auth-test` as the domain and `eu-west-1` as the region.
6 |
7 | Finally, you need to configure your app client. Go to App Integration -> App Client Settings.
8 |
9 | 1. Enable your newly created user pool under Enabled Identity Providers.
10 | 2. Add the callback url (must be same as in your config, e.g. `com.myclientapp://myclient/redirect`)
11 | 3. Enable the Authorization code grant
12 | 4. Enable openid scope
13 |
14 | ```js
15 | const config = {
16 | clientId: '',
17 | redirectUrl: 'com.myclientapp://myclient/redirect',
18 | serviceConfiguration: {
19 | authorizationEndpoint: '/oauth2/authorize',
20 | tokenEndpoint: '/oauth2/token',
21 | revocationEndpoint: '/oauth2/revoke',
22 | },
23 | };
24 |
25 | // Log in to get an authentication token
26 | const authState = await authorize(config);
27 |
28 | // Refresh token
29 | const refreshedState = await refresh(config, {
30 | refreshToken: authState.refreshToken,
31 | });
32 |
33 | // Revoke token
34 | await revoke(config, {
35 | tokenToRevoke: refreshedState.refreshToken,
36 | });
37 | ```
38 |
--------------------------------------------------------------------------------
/docs/src/theme/prism-include-languages.ts:
--------------------------------------------------------------------------------
1 | import siteConfig from '@generated/docusaurus.config';
2 | import * as PrismNamespace from 'prismjs';
3 | import { Optional } from 'utility-types';
4 | import { diffHighlight } from './prism-diff-highlight';
5 | import './prism-diff-highlight.css';
6 |
7 | const DIFF_LANGUAGE_REGEX = /^diff-([\w-]+)/i;
8 |
9 | export default function prismIncludeLanguages(PrismObject: typeof PrismNamespace): void {
10 | const {
11 | themeConfig: { prism },
12 | } = siteConfig;
13 | const { additionalLanguages } = prism as { additionalLanguages: string[] };
14 |
15 | // Prism components work on the Prism instance on the window, while prism-
16 | // react-renderer uses its own Prism instance. We temporarily mount the
17 | // instance onto window, import components to enhance it, then remove it to
18 | // avoid polluting global namespace.
19 | // You can mutate PrismObject: registering plugins, deleting languages... As
20 | // long as you don't re-assign it
21 | globalThis.Prism = PrismObject;
22 |
23 | additionalLanguages.forEach(lang => {
24 | const langMatch = DIFF_LANGUAGE_REGEX.exec(lang);
25 | if (langMatch) {
26 | // eslint-disable-next-line global-require
27 | if (!PrismObject.languages.diff) {
28 | console.error(
29 | 'prism-include-languages:',
30 | "You need to import 'diff' language first to use 'diff-xxxx' languages"
31 | );
32 | }
33 | PrismObject.languages[lang] = PrismObject.languages.diff;
34 | } else {
35 | // eslint-disable-next-line global-require, import/no-dynamic-require
36 | require(`prismjs/components/prism-${lang}`);
37 | }
38 | });
39 |
40 | diffHighlight(PrismObject);
41 |
42 | delete (globalThis as Optional).Prism;
43 | }
44 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "rnaa",
3 | "version": "1.0.0",
4 | "private": true,
5 | "description": "React Native bridge for AppAuth for supporting any OAuth 2 provider",
6 | "repository": {
7 | "type": "git",
8 | "url": "https://github.com/FormidableLabs/react-native-app-auth"
9 | },
10 | "bugs": {
11 | "url": "https://github.com/FormidableLabs/react-native-app-auth/issues"
12 | },
13 | "homepage": "https://github.com/FormidableLabs/react-native-app-auth",
14 | "keywords": [
15 | "react",
16 | "react-native",
17 | "auth",
18 | "authentication",
19 | "oauth",
20 | "oauth2",
21 | "appauth"
22 | ],
23 | "license": "MIT",
24 | "scripts": {
25 | "changeset": "changeset",
26 | "lint": "yarn workspace react-native-app-auth lint",
27 | "test": "yarn workspace react-native-app-auth test",
28 | "start:demo:android": "yarn workspace rnaa-demo android",
29 | "start:demo:ios": "yarn workspace rnaa-demo ios"
30 | },
31 | "devDependencies": {
32 | "@changesets/cli": "^2.26.1",
33 | "@svitejs/changesets-changelog-github-compact": "^0.1.1",
34 | "babel-eslint": "10.0.3",
35 | "eslint": "6.8.0",
36 | "eslint-config-formidable": "4.0.0",
37 | "eslint-config-prettier": "6.9.0",
38 | "eslint-plugin-filenames": "1.3.2",
39 | "eslint-plugin-import": "2.19.1",
40 | "eslint-plugin-jest": "23.3.0",
41 | "eslint-plugin-prettier": "3.1.2",
42 | "eslint-plugin-promise": "4.2.1",
43 | "prettier": "1.19.1"
44 | },
45 | "workspaces": {
46 | "packages": [
47 | "packages/*",
48 | "examples/*"
49 | ],
50 | "nohoist": [
51 | "**/react-native",
52 | "**/react-native/**",
53 | "**/@react-native",
54 | "**/@react-native/**",
55 | "**/react-native-app-auth/**"
56 | ]
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/docs/docs/providers/spotify.md:
--------------------------------------------------------------------------------
1 | # Spotify
2 |
3 | If you don't already have a spotify app, create it [here](https://developer.spotify.com/dashboard/applications).
4 |
5 | Open your app, go to settings and add a redirect uri, e.g. `com.myapp:/oauth`.
6 |
7 | Note: iOS redirect on Spotify only works with one `/`.s
8 |
9 | ```js
10 | const config = {
11 | clientId: '', // available on the app page
12 | clientSecret: '', // click "show client secret" to see this
13 | redirectUrl: 'com.myapp:/oauth', // the redirect you defined after creating the app
14 | scopes: ['user-read-email', 'playlist-modify-public', 'user-read-private'], // the scopes you need to access
15 | serviceConfiguration: {
16 | authorizationEndpoint: 'https://accounts.spotify.com/authorize',
17 | tokenEndpoint: 'https://accounts.spotify.com/api/token',
18 | },
19 | };
20 |
21 | const authState = await authorize(config);
22 | ```
23 |
24 | ## Managing Client Secrets
25 |
26 | In order to avoid storing the `clientSecret` in the client, Spotify has published a token exchange package that can be used to move this step to the backend:
27 | https://github.com/bih/spotify-token-swap-service
28 |
29 | The tokenEndpoint should then point to whereever you are hosting this server, and be sure to remove the secret from your app:
30 |
31 | ```js
32 | const config = {
33 | clientId: '', // available on the app page
34 | redirectUrl: 'com.myapp:/oauth', // the redirect you defined after creating the app
35 | scopes: ['user-read-email', 'playlist-modify-public', 'user-read-private'], // the scopes you need to access
36 | serviceConfiguration: {
37 | authorizationEndpoint: 'https://accounts.spotify.com/authorize',
38 | tokenEndpoint: 'https://my-token-service/api/token',
39 | },
40 | };
41 |
42 | const authState = await authorize(config);
43 | ```
44 |
--------------------------------------------------------------------------------
/packages/react-native-app-auth/android/src/main/java/com/rnappauth/utils/RegistrationResponseFactory.java:
--------------------------------------------------------------------------------
1 | package com.rnappauth.utils;
2 |
3 | import com.facebook.react.bridge.Arguments;
4 | import com.facebook.react.bridge.WritableMap;
5 |
6 | import net.openid.appauth.RegistrationResponse;
7 |
8 | public final class RegistrationResponseFactory {
9 | /*
10 | * Read raw registration response into a React Native map to be passed down the bridge
11 | */
12 | public static final WritableMap registrationResponseToMap(RegistrationResponse response) {
13 | WritableMap map = Arguments.createMap();
14 |
15 | map.putString("clientId", response.clientId);
16 | map.putMap("additionalParameters", MapUtil.createAdditionalParametersMap(response.additionalParameters));
17 |
18 | if (response.clientIdIssuedAt != null) {
19 | map.putString("clientIdIssuedAt", DateUtil.formatTimestamp(response.clientIdIssuedAt));
20 | }
21 |
22 | if (response.clientSecret != null) {
23 | map.putString("clientSecret", response.clientSecret);
24 | }
25 |
26 | if (response.clientSecretExpiresAt != null) {
27 | map.putString("clientSecretExpiresAt", DateUtil.formatTimestamp(response.clientSecretExpiresAt));
28 | }
29 |
30 | if (response.registrationAccessToken != null) {
31 | map.putString("registrationAccessToken", response.registrationAccessToken);
32 | }
33 |
34 | if (response.registrationClientUri != null) {
35 | map.putString("registrationClientUri", response.registrationClientUri.toString());
36 | }
37 |
38 | if (response.tokenEndpointAuthMethod != null) {
39 | map.putString("tokenEndpointAuthMethod", response.tokenEndpointAuthMethod);
40 | }
41 |
42 | return map;
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/docs/src/components/landing/landing-featured-projects.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { FeaturedBadge } from 'formidable-oss-badges';
3 | import { NFLinkButton } from './nf-link-button';
4 | import { Divider } from './divider';
5 |
6 | type featuredProject =
7 | | 'renature'
8 | | 'spectacle'
9 | | 'urql'
10 | | 'victory'
11 | | 'nuka'
12 | | 'owl'
13 | | 'groqd'
14 | | 'envy'
15 | | 'figlog';
16 |
17 | export const LandingFeaturedProjects = ({
18 | heading,
19 | projects,
20 | showDivider,
21 | }: {
22 | heading: string;
23 | projects: {
24 | name: featuredProject;
25 | link: string;
26 | description: string;
27 | title?: string;
28 | }[];
29 | showDivider?: boolean;
30 | }) => (
31 |
32 | {showDivider &&
}
33 |
{heading}
34 |
49 |
50 |
51 |
52 |
53 |
54 | );
55 |
--------------------------------------------------------------------------------
/docs/docs/providers/okta.md:
--------------------------------------------------------------------------------
1 | # Okta
2 |
3 | Full support out of the box.
4 |
5 | > If you're using Okta and want to add App Auth to your React Native application, you'll need an application to authorize against. If you don't have an Okta Developer account, [you can signup for free](https://developer.okta.com/signup/).
6 | >
7 | > Log in to your Okta Developer account and navigate to **Applications** > **Applications** > **Create App Integration**. Click **OIDC - OpenID Connect**, then **Native Application**, then click the **Next** button. Give the app integration a name you’ll remember (e.g., `React Native`), select `Refresh Token` as a grant type, in addition to the default `Authorization Code`. Copy the **Sign-in redirect URI** (e.g., `com.oktapreview.dev-158606:/callback`) and save it somewhere. You'll need this value when configuring your app.
8 | >
9 | > Click **Save** and you'll see a client ID on the next screen. Copy the redirect URI and clientId values into your App Auth config.
10 | >
11 | > To end the session, `postLogoutRedirectUrl` has to be one of the **Sign-out redirect URIs** defined in the **General Settings** > **LOGIN** section of the application page previously created.
12 |
13 | ```js
14 | const config = {
15 | issuer: 'https://{yourOktaDomain}.com/oauth2/default',
16 | clientId: '{clientId}',
17 | redirectUrl: 'com.{yourReversedOktaDomain}:/callback',
18 | scopes: ['openid', 'profile'],
19 | };
20 |
21 | // Log in to get an authentication token
22 | const authState = await authorize(config);
23 |
24 | // Refresh token
25 | const refreshedState = await refresh(config, {
26 | refreshToken: authState.refreshToken,
27 | });
28 |
29 | // Revoke token
30 | await revoke(config, {
31 | tokenToRevoke: refreshedState.refreshToken,
32 | });
33 |
34 | // End session
35 | await logout(config, {
36 | idToken: authState.idToken,
37 | postLogoutRedirectUrl: 'com.{yourReversedOktaDomain}:/logout',
38 | });
39 | ```
40 |
--------------------------------------------------------------------------------
/examples/demo/ios/Example/AppDelegate.mm:
--------------------------------------------------------------------------------
1 | #import "AppDelegate.h"
2 |
3 | #import
4 |
5 | @implementation AppDelegate
6 |
7 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
8 | {
9 | self.moduleName = @"Example";
10 | // You can add your custom initial props in the dictionary below.
11 | // They will be passed down to the ViewController used by React Native.
12 | self.initialProps = @{};
13 |
14 | return [super application:application didFinishLaunchingWithOptions:launchOptions];
15 | }
16 |
17 | - (BOOL) application: (UIApplication *)application
18 | openURL: (NSURL *)url
19 | options: (NSDictionary *) options
20 | {
21 | if ([self.authorizationFlowManagerDelegate resumeExternalUserAgentFlowWithURL:url]) {
22 | return YES;
23 | }
24 | return [RCTLinkingManager application:application openURL:url options:options];
25 | }
26 |
27 | - (BOOL) application: (UIApplication *) application
28 | continueUserActivity: (nonnull NSUserActivity *)userActivity
29 | restorationHandler: (nonnull void (^)(NSArray> * _Nullable))restorationHandler
30 | {
31 | if ([userActivity.activityType isEqualToString:NSUserActivityTypeBrowsingWeb]) {
32 | if (self.authorizationFlowManagerDelegate) {
33 | BOOL resumableAuth = [self.authorizationFlowManagerDelegate resumeExternalUserAgentFlowWithURL:userActivity.webpageURL];
34 | if (resumableAuth) {
35 | return YES;
36 | }
37 | }
38 | }
39 | return [RCTLinkingManager application:application continueUserActivity:userActivity restorationHandler:restorationHandler];
40 | }
41 |
42 | - (NSURL *)sourceURLForBridge:(RCTBridge *)bridge
43 | {
44 | #if DEBUG
45 | return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index"];
46 | #else
47 | return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
48 | #endif
49 | }
50 |
51 | @end
52 |
--------------------------------------------------------------------------------
/docs/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 |
3 | const NearFormColors = {
4 | White: 'hsla(0, 0%, 100%, 1)',
5 | Black: 'hsla(0, 0%, 0%, 1)',
6 | Green: 'hsla(163, 100%, 45%, 1)',
7 | Purple: 'hsla(260, 100%, 70%, 1)',
8 | LightPurple: 'hsla(262, 100%, 90%, 1)',
9 | Blue: 'hsla(218, 100%, 64%, 1)',
10 | LightBlue: 'hsla(217, 100%, 92%, 1)',
11 | Grey: 'hsla(0, 0%, 85%, 1)',
12 | LightGrey: '#F4F8FA',
13 | DeepGrey: 'hsla(240, 8%, 29%, 1)',
14 | Navy: 'hsla(205, 78%, 21%, 1)',
15 | LightNavy: 'hsla(222, 25%, 43%, 1)',
16 | DeepNavy: 'hsla(225, 100%, 11%, 1)',
17 | };
18 |
19 | module.exports = {
20 | corePlugins: {
21 | preflight: false, // disable Tailwind's reset
22 | },
23 | content: ['./src/**/*.{js,jsx,ts,tsx}', '../docs/**/*.mdx'],
24 | darkMode: ['class', '[data-theme="dark"]'],
25 | theme: {
26 | extend: {
27 | colors: {
28 | transparent: 'transparent',
29 | white: NearFormColors.White,
30 | black: NearFormColors.Black,
31 | grayscale: {
32 | 100: NearFormColors.White,
33 | 200: NearFormColors.LightGrey,
34 | 300: NearFormColors.Grey,
35 | 400: NearFormColors.DeepGrey,
36 | 500: NearFormColors.Black,
37 | 800: '#888888',
38 | },
39 | 'theme-1': NearFormColors.Green,
40 | 'theme-2': NearFormColors.DeepNavy,
41 | 'theme-3': NearFormColors.DeepNavy,
42 | 'theme-4': NearFormColors.White,
43 | 'header-bg': NearFormColors.White,
44 | 'header-fg': NearFormColors.DeepNavy,
45 | 'button-bg': NearFormColors.Green,
46 | 'button-fg': NearFormColors.DeepNavy,
47 | 'button-bg-hover': NearFormColors.White,
48 | 'button-fg-hover': NearFormColors.DeepNavy,
49 | 'button-border': NearFormColors.Green,
50 | error: '#FF0000',
51 | },
52 | width: {
53 | prose: '90ch',
54 | },
55 | fontFamily: {
56 | sans: ['Inter, Helvetica, Arial, sans-serif'],
57 | },
58 | },
59 | },
60 | };
61 |
--------------------------------------------------------------------------------
/examples/demo/android/app/src/main/res/drawable/rn_edit_text_material.xml:
--------------------------------------------------------------------------------
1 |
2 |
16 |
21 |
22 |
23 |
32 |
33 |
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/examples/demo/android/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 |
3 | # IDE (e.g. Android Studio) users:
4 | # Gradle settings configured through the IDE *will override*
5 | # any settings specified in this file.
6 |
7 | # For more details on how to configure your build environment visit
8 | # http://www.gradle.org/docs/current/userguide/build_environment.html
9 |
10 | # Specifies the JVM arguments used for the daemon process.
11 | # The setting is particularly useful for tweaking memory settings.
12 | # Default value: -Xmx512m -XX:MaxMetaspaceSize=256m
13 | org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m
14 |
15 | # When configured, Gradle will run in incubating parallel mode.
16 | # This option should only be used with decoupled projects. More details, visit
17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
18 | # org.gradle.parallel=true
19 |
20 | # AndroidX package structure to make it clearer which packages are bundled with the
21 | # Android operating system, and which are packaged with your app's APK
22 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
23 | android.useAndroidX=true
24 | # Automatically convert third-party libraries to use AndroidX
25 | android.enableJetifier=true
26 |
27 | # Version of flipper SDK to use with React Native
28 | FLIPPER_VERSION=0.182.0
29 |
30 | # Use this property to specify which architecture you want to build.
31 | # You can also override it from the CLI using
32 | # ./gradlew -PreactNativeArchitectures=x86_64
33 | reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64
34 |
35 | # Use this property to enable support to the new architecture.
36 | # This will allow you to use TurboModules and the Fabric render in
37 | # your application. You should enable this flag either if you want
38 | # to write custom TurboModules/Fabric components OR use libraries that
39 | # are providing them.
40 | newArchEnabled=false
41 |
42 | # Use this property to enable or disable the Hermes JS engine.
43 | # If set to false, you will be using JSC instead.
44 | hermesEnabled=true
45 |
--------------------------------------------------------------------------------
/examples/demo/android/app/src/main/java/com/example/MainApplication.java:
--------------------------------------------------------------------------------
1 | package com.example;
2 |
3 | import android.app.Application;
4 | import com.facebook.react.PackageList;
5 | import com.facebook.react.ReactApplication;
6 | import com.facebook.react.ReactNativeHost;
7 | import com.facebook.react.ReactPackage;
8 | import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint;
9 | import com.facebook.react.defaults.DefaultReactNativeHost;
10 | import com.facebook.soloader.SoLoader;
11 | import java.util.List;
12 |
13 | public class MainApplication extends Application implements ReactApplication {
14 |
15 | private final ReactNativeHost mReactNativeHost =
16 | new DefaultReactNativeHost(this) {
17 | @Override
18 | public boolean getUseDeveloperSupport() {
19 | return BuildConfig.DEBUG;
20 | }
21 |
22 | @Override
23 | protected List getPackages() {
24 | @SuppressWarnings("UnnecessaryLocalVariable")
25 | List packages = new PackageList(this).getPackages();
26 | // Packages that cannot be autolinked yet can be added manually here, for example:
27 | // packages.add(new MyReactNativePackage());
28 | return packages;
29 | }
30 |
31 | @Override
32 | protected String getJSMainModuleName() {
33 | return "index";
34 | }
35 |
36 | @Override
37 | protected boolean isNewArchEnabled() {
38 | return BuildConfig.IS_NEW_ARCHITECTURE_ENABLED;
39 | }
40 |
41 | @Override
42 | protected Boolean isHermesEnabled() {
43 | return BuildConfig.IS_HERMES_ENABLED;
44 | }
45 | };
46 |
47 | @Override
48 | public ReactNativeHost getReactNativeHost() {
49 | return mReactNativeHost;
50 | }
51 |
52 | @Override
53 | public void onCreate() {
54 | super.onCreate();
55 | SoLoader.init(this, /* native exopackage */ false);
56 | if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
57 | // If you opted-in for the New Architecture, we load the native entry point for this app.
58 | DefaultNewArchitectureEntryPoint.load();
59 | }
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/packages/react-native-app-auth/android/build.gradle:
--------------------------------------------------------------------------------
1 | def safeExtGet(prop, fallback) {
2 | rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
3 | }
4 |
5 | buildscript {
6 | // The Android Gradle plugin is only required when opening the android folder stand-alone.
7 | // This avoids unnecessary downloads and potential conflicts when the library is included as a
8 | // module dependency in an application project.
9 | if (project == rootProject) {
10 | repositories {
11 | mavenCentral()
12 | google()
13 | }
14 | dependencies {
15 | classpath 'com.android.tools.build:gradle:3.5.3'
16 | }
17 | }
18 | }
19 |
20 | apply plugin: 'com.android.library'
21 |
22 | android {
23 | def agpVersion = com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION
24 | if (agpVersion.tokenize('.')[0].toInteger() >= 7) {
25 | namespace "com.rnappauth"
26 | }
27 |
28 | compileSdkVersion safeExtGet('compileSdkVersion', 28)
29 | defaultConfig {
30 | minSdkVersion safeExtGet('minSdkVersion', 16)
31 | targetSdkVersion safeExtGet('targetSdkVersion', 26)
32 | versionCode 1
33 | versionName "1.0"
34 | manifestPlaceholders = [
35 | 'appAuthRedirectScheme': 'please.override.me'
36 | ]
37 | }
38 | lintOptions {
39 | abortOnError false
40 | }
41 | compileOptions {
42 | sourceCompatibility JavaVersion.VERSION_1_8
43 | targetCompatibility JavaVersion.VERSION_1_8
44 | }
45 | }
46 |
47 | repositories {
48 | mavenLocal()
49 | mavenCentral()
50 | maven {
51 | // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
52 | url "$rootDir/../node_modules/react-native/android"
53 | }
54 | maven {
55 | // Android JSC is installed from npm
56 | url "$rootDir/../node_modules/jsc-android/dist"
57 | }
58 | google()
59 | }
60 |
61 | dependencies {
62 | //noinspection GradleDynamicVersion
63 | implementation 'com.facebook.react:react-native:+' // From node_modules
64 | implementation 'net.openid:appauth:0.11.1'
65 | implementation 'androidx.browser:browser:1.4.0'
66 | }
67 |
--------------------------------------------------------------------------------
/examples/demo/ios/ExampleTests/ExampleTests.m:
--------------------------------------------------------------------------------
1 | #import
2 | #import
3 |
4 | #import
5 | #import
6 |
7 | #define TIMEOUT_SECONDS 600
8 | #define TEXT_TO_LOOK_FOR @"Welcome to React"
9 |
10 | @interface ExampleTests : XCTestCase
11 |
12 | @end
13 |
14 | @implementation ExampleTests
15 |
16 | - (BOOL)findSubviewInView:(UIView *)view matching:(BOOL (^)(UIView *view))test
17 | {
18 | if (test(view)) {
19 | return YES;
20 | }
21 | for (UIView *subview in [view subviews]) {
22 | if ([self findSubviewInView:subview matching:test]) {
23 | return YES;
24 | }
25 | }
26 | return NO;
27 | }
28 |
29 | - (void)testRendersWelcomeScreen
30 | {
31 | UIViewController *vc = [[[RCTSharedApplication() delegate] window] rootViewController];
32 | NSDate *date = [NSDate dateWithTimeIntervalSinceNow:TIMEOUT_SECONDS];
33 | BOOL foundElement = NO;
34 |
35 | __block NSString *redboxError = nil;
36 | #ifdef DEBUG
37 | RCTSetLogFunction(
38 | ^(RCTLogLevel level, RCTLogSource source, NSString *fileName, NSNumber *lineNumber, NSString *message) {
39 | if (level >= RCTLogLevelError) {
40 | redboxError = message;
41 | }
42 | });
43 | #endif
44 |
45 | while ([date timeIntervalSinceNow] > 0 && !foundElement && !redboxError) {
46 | [[NSRunLoop mainRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
47 | [[NSRunLoop mainRunLoop] runMode:NSRunLoopCommonModes beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
48 |
49 | foundElement = [self findSubviewInView:vc.view
50 | matching:^BOOL(UIView *view) {
51 | if ([view.accessibilityLabel isEqualToString:TEXT_TO_LOOK_FOR]) {
52 | return YES;
53 | }
54 | return NO;
55 | }];
56 | }
57 |
58 | #ifdef DEBUG
59 | RCTSetLogFunction(RCTDefaultLogFunction);
60 | #endif
61 |
62 | XCTAssertNil(redboxError, @"RedBox error: %@", redboxError);
63 | XCTAssertTrue(foundElement, @"Couldn't find element with text '%@' in %d seconds", TEXT_TO_LOOK_FOR, TIMEOUT_SECONDS);
64 | }
65 |
66 | @end
67 |
--------------------------------------------------------------------------------
/examples/demo/ios/Podfile:
--------------------------------------------------------------------------------
1 | # Resolve react_native_pods.rb with node to allow for hoisting
2 | require Pod::Executable.execute_command('node', ['-p',
3 | 'require.resolve(
4 | "react-native/scripts/react_native_pods.rb",
5 | {paths: [process.argv[1]]},
6 | )', __dir__]).strip
7 |
8 | platform :ios, min_ios_version_supported
9 | prepare_react_native_project!
10 |
11 | # If you are using a `react-native-flipper` your iOS build will fail when `NO_FLIPPER=1` is set.
12 | # because `react-native-flipper` depends on (FlipperKit,...) that will be excluded
13 | #
14 | # To fix this you can also exclude `react-native-flipper` using a `react-native.config.js`
15 | # ```js
16 | # module.exports = {
17 | # dependencies: {
18 | # ...(process.env.NO_FLIPPER ? { 'react-native-flipper': { platforms: { ios: null } } } : {}),
19 | # ```
20 | flipper_config = FlipperConfiguration.disabled
21 |
22 | linkage = ENV['USE_FRAMEWORKS']
23 | if linkage != nil
24 | Pod::UI.puts "Configuring Pod with #{linkage}ally linked Frameworks".green
25 | use_frameworks! :linkage => linkage.to_sym
26 | end
27 |
28 | target 'Example' do
29 | config = use_native_modules!
30 |
31 | # Flags change depending on the env values.
32 | flags = get_default_flags()
33 |
34 | use_react_native!(
35 | :path => config[:reactNativePath],
36 | # Hermes is now enabled by default. Disable by setting this flag to false.
37 | :hermes_enabled => flags[:hermes_enabled],
38 | :fabric_enabled => flags[:fabric_enabled],
39 | # Enables Flipper.
40 | #
41 | # Note that if you have use_frameworks! enabled, Flipper will not work and
42 | # you should disable the next line.
43 | :flipper_configuration => flipper_config,
44 | # An absolute path to your application root.
45 | :app_path => "#{Pod::Config.instance.installation_root}/.."
46 | )
47 |
48 | target 'ExampleTests' do
49 | inherit! :complete
50 | # Pods for testing
51 | end
52 |
53 | post_install do |installer|
54 | # https://github.com/facebook/react-native/blob/main/packages/react-native/scripts/react_native_pods.rb#L197-L202
55 | react_native_post_install(
56 | installer,
57 | config[:reactNativePath],
58 | :mac_catalyst_enabled => false
59 | )
60 | __apply_Xcode_12_5_M1_post_install_workaround(installer)
61 | end
62 | end
63 |
--------------------------------------------------------------------------------
/examples/demo/android/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
12 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/examples/demo/ios/Example/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleDisplayName
8 | Example
9 | CFBundleExecutable
10 | $(EXECUTABLE_NAME)
11 | CFBundleIdentifier
12 | $(PRODUCT_BUNDLE_IDENTIFIER)
13 | CFBundleInfoDictionaryVersion
14 | 6.0
15 | CFBundleName
16 | $(PRODUCT_NAME)
17 | CFBundlePackageType
18 | APPL
19 | CFBundleShortVersionString
20 | $(MARKETING_VERSION)
21 | CFBundleSignature
22 | ????
23 | CFBundleURLTypes
24 |
25 |
26 | CFBundleURLName
27 | com.your.app.identifier
28 | CFBundleURLSchemes
29 |
30 | io.identityserver.demo
31 |
32 |
33 |
34 | CFBundleTypeRole
35 | Editor
36 | CFBundleURLName
37 | com.your.app.identifier
38 | CFBundleURLSchemes
39 |
40 | rnaa-demo
41 |
42 |
43 |
44 | CFBundleVersion
45 | $(CURRENT_PROJECT_VERSION)
46 | LSRequiresIPhoneOS
47 |
48 | NSAppTransportSecurity
49 |
50 | NSExceptionDomains
51 |
52 | localhost
53 |
54 | NSExceptionAllowsInsecureHTTPLoads
55 |
56 |
57 |
58 |
59 | NSLocationWhenInUseUsageDescription
60 |
61 | UILaunchStoryboardName
62 | LaunchScreen
63 | UIRequiredDeviceCapabilities
64 |
65 | armv7
66 |
67 | UISupportedInterfaceOrientations
68 |
69 | UIInterfaceOrientationPortrait
70 | UIInterfaceOrientationLandscapeLeft
71 | UIInterfaceOrientationLandscapeRight
72 |
73 | UIViewControllerBasedStatusBarAppearance
74 |
75 |
76 |
77 |
--------------------------------------------------------------------------------
/docs/docs/token-storage.md:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar_position: 2
3 | ---
4 |
5 | # Token Storage
6 |
7 | Once the user has successfully authenticated, you'll have a JWT and possibly a refresh token that should be stored securely.
8 |
9 | ❗️ **Do not use Async Storage for storing sensitive information**
10 |
11 | Async Storage is the simplest method of persisting data across application launches in React Native. However, it is an _unencrypted_ key-value store and should therefore not be used for token storage.
12 |
13 | ✅ **DO use Secure Storage**
14 |
15 | React Native does not come bundled with any way of storing sensitive data, so it is necessary to rely on the underlying platform-specific solutions.
16 |
17 | ### iOS - Keychain Services
18 |
19 | Keychain Services allows you to securely store small chunks of sensitive info for the user. This is an ideal place to store certificates, tokens, passwords, and any other sensitive information that doesn’t belong in Async Storage.
20 |
21 | ### Android - Secure Shared Preferences
22 |
23 | Shared Preferences is the Android equivalent for a persistent key-value data store. Data in Shared Preferences is not encrypted by default. Encrypted Shared Preferences wraps the Shared Preferences class for Android, and automatically encrypts keys and values.
24 |
25 | In order to use iOS's Keychain services or Android's Secure Shared Preferences, you either can write a JS < - > native interface yourself or use a library which wraps them for you. Some even provide a unified API.
26 |
27 | ## Related OSS libraries
28 |
29 | - [react-native-keychain](https://github.com/oblador/react-native-keychain) - we've had good experiences using this on projects
30 | - [react-native-sensitive-info](https://github.com/mCodex/react-native-sensitive-info) - secure for iOS, but uses Android Shared Preferences for Android (which is not secure). There is however a fork that uses [Android Keystore](https://github.com/mCodex/react-native-sensitive-info/tree/keystore) which is secure
31 | - [redux-persist-sensitive-storage](https://github.com/CodingZeal/redux-persist-sensitive-storage) - wraps `react-native-sensitive-info`, see comments above
32 | - [rn-secure-storage](https://github.com/talut/rn-secure-storage)
33 | - [expo-secure-store](https://github.com/expo/expo/tree/master/packages/expo-secure-store) - secure for iOS by using keychain services, secure for Android by using values in SharedPreferences encrypted with Android's Keystore system. This Expo library can be used in "Managed" and "Bare" workflow apps, but note that when using this Expo library with React Native App Auth, only the bare workflow is supported.
34 |
--------------------------------------------------------------------------------
/packages/react-native-app-auth/android/src/main/java/com/rnappauth/utils/CustomConnectionBuilder.java:
--------------------------------------------------------------------------------
1 | package com.rnappauth.utils;
2 |
3 | /*
4 | * Copyright 2016 The AppAuth for Android Authors. All Rights Reserved.
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
7 | * in compliance with the License. You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software distributed under the
12 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
13 | * express or implied. See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 |
18 | import android.net.Uri;
19 | import androidx.annotation.NonNull;
20 |
21 | import net.openid.appauth.connectivity.ConnectionBuilder;
22 |
23 | import java.io.IOException;
24 | import java.net.HttpURLConnection;
25 | import java.util.concurrent.TimeUnit;
26 | import java.util.Map;
27 |
28 |
29 | /**
30 | * An implementation of {@link ConnectionBuilder} that permits
31 | * to set custom headers on connection use to request endpoints.
32 | * Useful for non-spec compliant oauth providers.
33 | */
34 | public final class CustomConnectionBuilder implements ConnectionBuilder {
35 |
36 | private Map headers = null;
37 |
38 | private int connectionTimeoutMs = (int) TimeUnit.SECONDS.toMillis(15);
39 | private int readTimeoutMs = (int) TimeUnit.SECONDS.toMillis(10);
40 | private ConnectionBuilder connectionBuilder;
41 |
42 | public CustomConnectionBuilder(ConnectionBuilder connectionBuilderToUse) {
43 | connectionBuilder = connectionBuilderToUse;
44 | }
45 |
46 | public void setHeaders (Map headersToSet) {
47 | headers = headersToSet;
48 | }
49 |
50 | public void setConnectionTimeout (int timeout) {
51 | connectionTimeoutMs = timeout;
52 | readTimeoutMs = timeout;
53 | }
54 |
55 | @NonNull
56 | @Override
57 | public HttpURLConnection openConnection(@NonNull Uri uri) throws IOException {
58 | HttpURLConnection conn = connectionBuilder.openConnection(uri);
59 |
60 | if (headers != null) {
61 | for (Map.Entry header: headers.entrySet()) {
62 | conn.setRequestProperty(header.getKey(), header.getValue());
63 | }
64 | }
65 |
66 | conn.setConnectTimeout(connectionTimeoutMs);
67 | conn.setReadTimeout(readTimeoutMs);
68 |
69 | return conn;
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/docs/docs/providers/azure-active-directory.md:
--------------------------------------------------------------------------------
1 | # Azure Active Directory
2 |
3 | Azure Active directory has two OAuth endpoints - [v1 and v2](https://docs.microsoft.com/en-us/azure/active-directory/develop/azure-ad-endpoint-comparison). Ideally, you'd want to use v2, but it has [some limitations](https://docs.microsoft.com/en-us/azure/active-directory/develop/azure-ad-endpoint-comparison#limitations), e.g. if your application relies on SAML, you'll have to use v1.
4 |
5 | ## V1
6 |
7 | The main difference between v1 and v2 is that v1 uses _resources_ and v2 uses _scopes_ for access management.
8 |
9 | V1 [does not specify a revocation endpoint](https://docs.microsoft.com/en-us/azure/active-directory/active-directory-configurable-token-lifetimes#access-tokens) because the access token are not revokable. Therefore `revoke` functionality doesn't work.
10 |
11 | See the [Azure docs on requesting an access token](https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-protocols-oauth-code#request-an-authorization-code) for more info on additional parameters.
12 |
13 | Please Note:
14 |
15 | - `Scopes` is ignored.
16 | - `additionalParameters.resource` may be required based on the tenant settings.
17 |
18 | ```js
19 | const config = {
20 | issuer: 'https://login.microsoftonline.com/your-tenant-id',
21 | clientId: 'your-client-id',
22 | redirectUrl: 'com.myapp://oauth/redirect/',
23 | additionalParameters: {
24 | resource: 'your-resource',
25 | },
26 | };
27 |
28 | // Log in to get an authentication token
29 | const authState = await authorize(config);
30 |
31 | // Refresh token
32 | const refreshedState = await refresh(config, {
33 | refreshToken: authState.refreshToken,
34 | });
35 | ```
36 |
37 | ## V2
38 |
39 | The V2 endpoint follows the standard OAuth protocol with scopes. Detailed documentation [here](https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-overview).
40 |
41 | ```js
42 | const config = {
43 | issuer: 'https://login.microsoftonline.com/your-tenant-id/v2.0',
44 | clientId: 'your-client-id',
45 | redirectUrl: 'com.myapp://oauth/redirect/',
46 | scopes: ['openid', 'profile', 'email', 'offline_access'],
47 | };
48 |
49 | // Log in to get an authentication token
50 | const authState = await authorize(config);
51 |
52 | // Refresh token
53 | const refreshedState = await refresh(config, {
54 | refreshToken: authState.refreshToken,
55 | });
56 | ```
57 |
58 | **Important** When you add your app in the azure portal and are given a `redirectUrl` to use, make sure you add a trailing slash when you add it to your config - e.g. `msauth.BUNDLEID://auth/` - failure to add that causes it to fail in IOS.
59 |
60 | **Logout:** To properly implement the `logout` functionality, please refer to the necessary requirements outlined in [this comment](https://github.com/FormidableLabs/react-native-app-auth/issues/715#issuecomment-1057444218).
61 |
--------------------------------------------------------------------------------
/docs/src/components/landing/landing-hero.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { ProjectBadge } from 'formidable-oss-badges';
3 |
4 | export const LandingHero = ({
5 | body,
6 | copyText,
7 | heading,
8 | navItems,
9 | }: {
10 | body: string;
11 | copyText: string;
12 | heading: string;
13 | navItems: { link: string; title: string }[];
14 | }) => {
15 | const [buttonText, setButtonText] = useState('Copy');
16 |
17 | const changeText = text => {
18 | setButtonText(text);
19 | };
20 |
21 | return (
22 |
23 |
24 |
25 |
33 |
34 |
{heading}
35 |
{body}
36 |
37 |
51 |
52 |
63 |
64 |
65 |
66 |
67 | );
68 | };
69 |
--------------------------------------------------------------------------------
/docs/src/theme/prism-diff-highlight.ts:
--------------------------------------------------------------------------------
1 | import { EnvConfig, PrismLib } from 'prism-react-renderer';
2 | import { TokenStream, Token } from 'prismjs';
3 |
4 | const LANGUAGE_REGEX = /^diff-([\w-]+)/i;
5 |
6 | const tokenStreamToString = (tokenStream: TokenStream): string => {
7 | const result: string[] = [];
8 | const stack: TokenStream[] = [tokenStream];
9 |
10 | while (stack.length > 0) {
11 | const item = stack.pop();
12 |
13 | if (typeof item === 'string') {
14 | result.push(item);
15 | } else if (Array.isArray(item)) {
16 | for (let i = item.length - 1; i >= 0; i--) {
17 | stack.push(item[i]);
18 | }
19 | } else {
20 | // If it's a Token, convert it to a string and push it
21 | stack.push(item.content);
22 | }
23 | }
24 |
25 | return result.join('');
26 | };
27 |
28 | export function diffHighlight(Prism: PrismLib) {
29 | Prism.hooks.add('after-tokenize', function(env: EnvConfig) {
30 | let diffLanguage;
31 | let diffGrammar;
32 | const language = env.language;
33 | if (language !== 'diff') {
34 | const langMatch = LANGUAGE_REGEX.exec(language);
35 | if (!langMatch) {
36 | return; // not a language specific diff
37 | }
38 |
39 | diffLanguage = langMatch[1];
40 | diffGrammar = Prism.languages[diffLanguage];
41 | if (!diffGrammar) {
42 | console.error(
43 | 'prism-diff-highlight:',
44 | `You need to add language '${diffLanguage}' to use '${language}'`
45 | );
46 | return;
47 | }
48 | } else return;
49 |
50 | const newTokens = [];
51 | env.tokens.forEach(token => {
52 | if (typeof token === 'string') {
53 | newTokens.push(...Prism.tokenize(token, diffGrammar));
54 | } else if (token.type === 'unchanged') {
55 | newTokens.push(...Prism.tokenize(tokenStreamToString(token), diffGrammar));
56 | } else if (['deleted-sign', 'inserted-sign'].includes(token.type)) {
57 | token.alias = [
58 | token.type === 'deleted-sign' ? 'diff-highlight-deleted' : 'diff-highlight-inserted',
59 | ];
60 | // diff parser always return "deleted" and "inserted" lines with content of type array
61 | if (token.content.length > 1) {
62 | const newTokenContent: Array = [];
63 | // preserve prefixes and don't parse them again
64 | // subTokens from diff parser are of type Token
65 | (token.content as Array).forEach((subToken: Token) => {
66 | if (subToken.type === 'prefix') {
67 | newTokenContent.push(subToken);
68 | } else {
69 | newTokenContent.push(...Prism.tokenize(tokenStreamToString(subToken), diffGrammar));
70 | }
71 | });
72 | token.content = newTokenContent;
73 | }
74 | newTokens.push(token);
75 | } else if (token.type === 'coord') {
76 | newTokens.push(token);
77 | }
78 | });
79 | console.log(newTokens);
80 | env.tokens = newTokens;
81 | });
82 | }
83 |
--------------------------------------------------------------------------------
/docs/docs/usage/register.md:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar_position: 4
3 | ---
4 |
5 | # Registration
6 |
7 | This will perform [dynamic client registration](https://openid.net/specs/openid-connect-registration-1_0.html) on the given provider.
8 | If the provider supports dynamic client registration, it will generate a `clientId` for you to use in subsequent calls to this library.
9 |
10 | ```js
11 | import { register } from 'react-native-app-auth';
12 |
13 | const registerConfig = {
14 | issuer: '',
15 | redirectUrls: ['', ''],
16 | };
17 |
18 | const registerResult = await register(registerConfig);
19 | ```
20 |
21 | #### registerConfig
22 |
23 | - **issuer** - (`string`) same as in authorization config
24 | - **serviceConfiguration** - (`object`) same as in authorization config
25 | - **redirectUrls** - (`array`) _REQUIRED_ specifies all of the redirect urls that your client will use for authentication
26 | - **responseTypes** - (`array`) 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']`
27 | - **grantTypes** - (`array`) 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']`
28 | - **subjectType** - (`string`) requests a specific [subject type](https://openid.net/specs/openid-connect-core-1_0.html#SubjectIDTypes) for your client
29 | - **tokenEndpointAuthMethod** (`string`) specifies which `clientAuthMethod` your client will use for authentication. The default value is `'client_secret_basic'`
30 | - **additionalParameters** - (`object`) additional parameters that will be passed in the registration request.
31 | Must be string values! E.g. setting `additionalParameters: { hello: 'world', foo: 'bar' }` would add
32 | `hello=world&foo=bar` to the authorization request.
33 | - **dangerouslyAllowInsecureHttpRequests** - (`boolean`) _ANDROID_ same as in authorization config
34 | - **customHeaders** - (`object`) _ANDROID_ same as in authorization config
35 | - **connectionTimeoutSeconds** - (`number`) configure the request timeout interval in seconds. This must be a positive number. The default values are 60 seconds on iOS and 15 seconds on Android.
36 |
37 | #### registerResult
38 |
39 | This is the result from the auth server
40 |
41 | - **clientId** - (`string`) the assigned client id
42 | - **clientIdIssuedAt** - (`string`) _OPTIONAL_ date string of when the client id was issued
43 | - **clientSecret** - (`string`) _OPTIONAL_ the assigned client secret
44 | - **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
45 | - **registrationClientUri** - (`string`) _OPTIONAL_ uri that can be used to perform subsequent operations on the registration
46 | - **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
47 |
--------------------------------------------------------------------------------
/examples/demo/android/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%"=="" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%"=="" set DIRNAME=.
29 | @rem This is normally unused
30 | set APP_BASE_NAME=%~n0
31 | set APP_HOME=%DIRNAME%
32 |
33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
35 |
36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
38 |
39 | @rem Find java.exe
40 | if defined JAVA_HOME goto findJavaFromJavaHome
41 |
42 | set JAVA_EXE=java.exe
43 | %JAVA_EXE% -version >NUL 2>&1
44 | if %ERRORLEVEL% equ 0 goto execute
45 |
46 | echo.
47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
48 | echo.
49 | echo Please set the JAVA_HOME variable in your environment to match the
50 | echo location of your Java installation.
51 |
52 | goto fail
53 |
54 | :findJavaFromJavaHome
55 | set JAVA_HOME=%JAVA_HOME:"=%
56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
57 |
58 | if exist "%JAVA_EXE%" goto execute
59 |
60 | echo.
61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
62 | echo.
63 | echo Please set the JAVA_HOME variable in your environment to match the
64 | echo location of your Java installation.
65 |
66 | goto fail
67 |
68 | :execute
69 | @rem Setup the command line
70 |
71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
72 |
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if %ERRORLEVEL% equ 0 goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | set EXIT_CODE=%ERRORLEVEL%
85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1
86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
87 | exit /b %EXIT_CODE%
88 |
89 | :mainEnd
90 | if "%OS%"=="Windows_NT" endlocal
91 |
92 | :omega
93 |
--------------------------------------------------------------------------------
/docs/docusaurus.config.ts:
--------------------------------------------------------------------------------
1 | import { themes as prismThemes } from 'prism-react-renderer';
2 | import { Config } from '@docusaurus/types';
3 |
4 | const config: Config = {
5 | title: 'React Native App Auth',
6 | tagline: 'React native bridge for AppAuth - an SDK for communicating with OAuth2 providers.',
7 | favicon: 'img/nearform-icon.svg',
8 | url: 'https://commerce.nearform.com/',
9 | baseUrl: '/open-source/react-native-app-auth',
10 | onBrokenLinks: 'throw',
11 | onBrokenMarkdownLinks: 'warn',
12 | i18n: {
13 | defaultLocale: 'en',
14 | locales: ['en'],
15 | },
16 | presets: [
17 | [
18 | 'classic',
19 | /** @type {import('@docusaurus/preset-classic').Options} */
20 | {
21 | docs: {
22 | sidebarPath: './sidebars.ts',
23 | },
24 | theme: {
25 | customCss: './src/css/custom.css',
26 | },
27 | gtag: {
28 | trackingID: "G-M971D063B9",
29 | },
30 | },
31 | ],
32 | ],
33 | themes: [
34 | [
35 | require.resolve('@easyops-cn/docusaurus-search-local'),
36 | /** @type {import("@easyops-cn/docusaurus-search-local").PluginOptions} */
37 | {
38 | hashed: true,
39 | indexBlog: false,
40 | },
41 | ],
42 | ],
43 | plugins: [
44 | async function myPlugin() {
45 | return {
46 | name: 'tailwind-plugin',
47 | configurePostCss(postcssOptions) {
48 | postcssOptions.plugins = [
49 | require('postcss-import'),
50 | require('tailwindcss'),
51 | require('autoprefixer'),
52 | ];
53 | return postcssOptions;
54 | },
55 | };
56 | },
57 | ],
58 | themeConfig: {
59 | /** @type {import('@docusaurus/preset-classic').ThemeConfig} */
60 | metadata: [
61 | { name: 'viewport', content: 'width=device-width, initial-scale=1, maximum-scale=1' },
62 | ],
63 | docs: {
64 | sidebar: {
65 | hideable: true,
66 | },
67 | },
68 | navbar: {
69 | title: 'React Native App Auth',
70 | logo: {
71 | alt: 'NearForm Logo',
72 | src: 'img/nearform-logo-white.svg',
73 | },
74 | items: [
75 | {
76 | type: 'docSidebar',
77 | sidebarId: 'sidebar',
78 | position: 'left',
79 | label: 'Documentation',
80 | },
81 | {
82 | href: 'https://github.com/FormidableLabs/react-native-app-auth',
83 | 'aria-label': 'GitHub Repository',
84 | className: 'header-github-link',
85 | position: 'right',
86 | },
87 | ],
88 | },
89 | footer: {
90 | logo: {
91 | alt: 'Nearform logo',
92 | src: 'img/nearform-logo-white.svg',
93 | href: 'https://commerce.nearform.com',
94 | width: 100,
95 | height: 100,
96 | },
97 | copyright: `Copyright © 2013-${new Date().getFullYear()} Nearform`,
98 | },
99 | prism: {
100 | theme: prismThemes.github,
101 | darkTheme: prismThemes.dracula,
102 | additionalLanguages: ['diff', 'diff-ts'],
103 | },
104 | },
105 | };
106 |
107 | export default config;
108 |
--------------------------------------------------------------------------------
/.github/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to making participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, gender identity and expression, level of experience,
9 | nationality, personal appearance, race, religion, or sexual identity and
10 | orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | * Using welcoming and inclusive language
18 | * Being respectful of differing viewpoints and experiences
19 | * Gracefully accepting constructive criticism
20 | * Focusing on what is best for the community
21 | * Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | * The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | * Trolling, insulting/derogatory comments, and personal or political attacks
28 | * Public or private harassment
29 | * Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | * Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies both within project spaces and in public spaces
49 | when an individual is representing the project or its community. Examples of
50 | representing a project or community include using an official project e-mail
51 | address, posting via an official social media account, or acting as an appointed
52 | representative at an online or offline event. Representation of a project may be
53 | further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team at kadi.kraman@formidable.com. All
59 | complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available at [http://contributor-covenant.org/version/1/4][version]
72 |
73 | [homepage]: http://contributor-covenant.org
74 | [version]: http://contributor-covenant.org/version/1/4/
75 |
--------------------------------------------------------------------------------
/docs/src/pages/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
3 | import Layout from '@theme/Layout';
4 |
5 | import { LandingHero } from '../components/landing/landing-hero';
6 | import { LandingBanner } from '../components/landing/landing-banner';
7 | import { LandingFeaturedProjects } from '../components/landing/landing-featured-projects';
8 | import { LandingFeatures } from '../components/landing/landing-features';
9 |
10 | export default function Home(): JSX.Element {
11 | const { siteConfig } = useDocusaurusContext();
12 | return (
13 |
14 |
15 |
31 |
32 |
41 |
47 |
80 |
81 | );
82 | }
83 |
--------------------------------------------------------------------------------
/packages/react-native-app-auth/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # react-native-app-auth
2 |
3 | ## 8.0.1
4 |
5 | ### Patch Changes
6 |
7 | - Update AppAuth-iOS to version 1.7.6 ([#1039](https://github.com/FormidableLabs/react-native-app-auth/pull/1039))
8 |
9 | ## 8.0.0
10 |
11 | ### Major Changes
12 |
13 | - Breaking change (iOS, Mac Catalyst): The boolean values `useNonce`, `usePCKE`, and `prefersEphemeralSession` are now handled correctly. Previously, they were all being interpreted as `false` regardless of their actual values, but now the intended (`true` or `false`) value is correctly marshalled from JavaScript to native. To preserve behaviour from before this breaking change, explicitly set them all to `false`. ([#1000](https://github.com/FormidableLabs/react-native-app-auth/pull/1000))
14 |
15 | ### Patch Changes
16 |
17 | - fix hard crash if config object was incorrect ([#1010](https://github.com/FormidableLabs/react-native-app-auth/pull/1010))
18 |
19 | ## 7.2.0
20 |
21 | ### Minor Changes
22 |
23 | - Updated the minimum version of AppAuth-iOS to 1.7.3 to meet the package's requirement, which includes the necessary privacy manifest. ([#971](https://github.com/FormidableLabs/react-native-app-auth/pull/971))
24 |
25 | ## 7.1.3
26 |
27 | ### Patch Changes
28 |
29 | - Moves '@changesets/cli' from dependencies to devDependencies, so that it isn't downloaded for react-native-app-auth package users ([#945](https://github.com/FormidableLabs/react-native-app-auth/pull/945))
30 |
31 | ## 7.1.2
32 |
33 | ### Patch Changes
34 |
35 | - Fix iosCustomBrowser not exchanging token ([`cb3b70a`](https://github.com/FormidableLabs/react-native-app-auth/commit/cb3b70a24cc02f46c72805a933ece66726e72213))
36 |
37 | ## 7.1.1
38 |
39 | ### Patch Changes
40 |
41 | - Fix Android crash with NullPointerException ([`a437123`](https://github.com/FormidableLabs/react-native-app-auth/commit/a4371235f37894e2aede6645efef95cf26e4143f))
42 |
43 | ## 7.1.0
44 |
45 | ### Minor Changes
46 |
47 | - Added `androidTrustedWebActivity` config to opt-in to EXTRA_LAUNCH_AS_TRUSTED_WEB_ACTIVITY ([#908](https://github.com/FormidableLabs/react-native-app-auth/pull/908))
48 |
49 | ## 7.0.0
50 |
51 | ### Minor Changes
52 |
53 | - Added support for Chrome Trusted Web Activity ([#897](https://github.com/FormidableLabs/react-native-app-auth/pull/897))
54 |
55 | ### Patch Changes
56 |
57 | - Fix order of parameters for register on iOS ([#804](https://github.com/FormidableLabs/react-native-app-auth/pull/804))
58 |
59 | * Readme update for RN 0.68+ setup ([#900](https://github.com/FormidableLabs/react-native-app-auth/pull/900))
60 |
61 | - Update README to link to Contributing guide ([#887](https://github.com/FormidableLabs/react-native-app-auth/pull/887))
62 |
63 | * correct swift setup example code ([#775](https://github.com/FormidableLabs/react-native-app-auth/pull/775))
64 |
65 | - Improve readability of method arguments be renaming `headers` argument to `customHeaders` ([#899](https://github.com/FormidableLabs/react-native-app-auth/pull/899))
66 |
67 | * Fix support of setAdditionalParameters on logout method on Android ([#765](https://github.com/FormidableLabs/react-native-app-auth/pull/765))
68 |
69 | - Update the Example app to RN 0.72 ([#896](https://github.com/FormidableLabs/react-native-app-auth/pull/896))
70 |
71 | * Fix authorization state parameter in iOS when using custom configuration ([#847](https://github.com/FormidableLabs/react-native-app-auth/pull/847))
72 |
73 | - Adding GitHub release workflow ([#853](https://github.com/FormidableLabs/react-native-app-auth/pull/853))
74 |
75 | * Added Asgardeo configuration example ([#882](https://github.com/FormidableLabs/react-native-app-auth/pull/882))
76 |
--------------------------------------------------------------------------------
/docs/src/css/custom.css:
--------------------------------------------------------------------------------
1 | /**
2 | * Any CSS included here will be global. The classic template
3 | * bundles Infima by default. Infima is a CSS framework designed to
4 | * work well for content-centric websites.
5 | */
6 | @tailwind base;
7 | @tailwind components;
8 | @tailwind utilities;
9 |
10 | body {
11 | scroll-behavior: smooth;
12 | text-rendering: optimizeSpeed;
13 | }
14 |
15 | @font-face {
16 | font-family: 'Inter';
17 | src: url('/font/InterRegular.woff2') format('woff2');
18 | font-weight: 400;
19 | font-style: normal;
20 | font-display: swap;
21 | }
22 |
23 | @font-face {
24 | font-family: 'Inter';
25 | src: url('/font/InterMedium.woff2') format('woff2');
26 | font-weight: 500;
27 | font-style: normal;
28 | font-display: swap;
29 | }
30 |
31 | @font-face {
32 | font-family: 'Inter';
33 | src: url('/font/InterBold.woff2') format('woff2');
34 | font-weight: 700;
35 | font-style: normal;
36 | font-display: swap;
37 | }
38 |
39 | .hero-pattern {
40 | background-image: url('/img/hero-pattern.png');
41 | }
42 |
43 | :root {
44 | --ifm-color-primary: #5abdee;
45 | --ifm-color-primary-dark: #3cb1eb;
46 | --ifm-color-primary-darker: #2dabe9;
47 | --ifm-color-primary-darkest: #1592d0;
48 | --ifm-color-primary-light: #78c9f1;
49 | --ifm-color-primary-lighter: #87cff3;
50 | --ifm-color-primary-lightest: #b3e1f7;
51 | --ifm-code-font-size: 95%;
52 | --ifm-list-item-margin: 0;
53 | --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1);
54 | --ifm-navbar-background-color: #000e38;
55 | --ifm-navbar-link-color: #ffffff;
56 | --ifm-footer-background-color: #000e38;
57 | --ifm-footer-padding-vertical: 1rem;
58 | }
59 |
60 | [data-theme='dark'] {
61 | --ifm-color-primary: #5abdee;
62 | --ifm-color-primary-dark: #3cb1eb;
63 | --ifm-color-primary-darker: #2dabe9;
64 | --ifm-color-primary-darkest: #1592d0;
65 | --ifm-color-primary-light: #78c9f1;
66 | --ifm-color-primary-lighter: #87cff3;
67 | --ifm-color-primary-lightest: #b3e1f7;
68 | --ifm-list-item-margin: 0;
69 | --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3);
70 | --ifm-navbar-background-color: #242526;
71 | --ifm-footer-background-color: #242526;
72 | }
73 |
74 | /* Nav */
75 | .header-github-link::before {
76 | content: '';
77 | width: 24px;
78 | height: 24px;
79 | display: flex;
80 | background: url("data:image/svg+xml;charset=utf-8,%3Csvg fill='white' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12'/%3E%3C/svg%3E")
81 | no-repeat;
82 | }
83 |
84 | .navbar__inner button svg {
85 | color: white;
86 | }
87 |
88 | /* Custom Footer */
89 | .footer__bottom.text--center {
90 | display: flex;
91 | justify-content: space-between;
92 | align-items: center;
93 | }
94 |
95 | .footer__bottom.text--center a {
96 | opacity: 1 !important;
97 | }
98 |
99 | .footer__copyright {
100 | color: white;
101 | }
102 |
--------------------------------------------------------------------------------
/examples/demo/ios/Example.xcodeproj/xcshareddata/xcschemes/Example.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
33 |
39 |
40 |
41 |
42 |
43 |
53 |
55 |
61 |
62 |
63 |
64 |
70 |
72 |
78 |
79 |
80 |
81 |
83 |
84 |
87 |
88 |
89 |
--------------------------------------------------------------------------------
/examples/demo/android/app/src/debug/java/com/example/ReactNativeFlipper.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) Meta Platforms, Inc. and affiliates.
3 | *
4 | * This source code is licensed under the MIT license found in the LICENSE file in the root
5 | * directory of this source tree.
6 | */
7 | package com.example;
8 |
9 | import android.content.Context;
10 | import com.facebook.flipper.android.AndroidFlipperClient;
11 | import com.facebook.flipper.android.utils.FlipperUtils;
12 | import com.facebook.flipper.core.FlipperClient;
13 | import com.facebook.flipper.plugins.crashreporter.CrashReporterPlugin;
14 | import com.facebook.flipper.plugins.databases.DatabasesFlipperPlugin;
15 | import com.facebook.flipper.plugins.fresco.FrescoFlipperPlugin;
16 | import com.facebook.flipper.plugins.inspector.DescriptorMapping;
17 | import com.facebook.flipper.plugins.inspector.InspectorFlipperPlugin;
18 | import com.facebook.flipper.plugins.network.FlipperOkhttpInterceptor;
19 | import com.facebook.flipper.plugins.network.NetworkFlipperPlugin;
20 | import com.facebook.flipper.plugins.sharedpreferences.SharedPreferencesFlipperPlugin;
21 | import com.facebook.react.ReactInstanceEventListener;
22 | import com.facebook.react.ReactInstanceManager;
23 | import com.facebook.react.bridge.ReactContext;
24 | import com.facebook.react.modules.network.NetworkingModule;
25 | import okhttp3.OkHttpClient;
26 |
27 | /**
28 | * Class responsible of loading Flipper inside your React Native application. This is the debug
29 | * flavor of it. Here you can add your own plugins and customize the Flipper setup.
30 | */
31 | public class ReactNativeFlipper {
32 | public static void initializeFlipper(Context context, ReactInstanceManager reactInstanceManager) {
33 | if (FlipperUtils.shouldEnableFlipper(context)) {
34 | final FlipperClient client = AndroidFlipperClient.getInstance(context);
35 |
36 | client.addPlugin(new InspectorFlipperPlugin(context, DescriptorMapping.withDefaults()));
37 | client.addPlugin(new DatabasesFlipperPlugin(context));
38 | client.addPlugin(new SharedPreferencesFlipperPlugin(context));
39 | client.addPlugin(CrashReporterPlugin.getInstance());
40 |
41 | NetworkFlipperPlugin networkFlipperPlugin = new NetworkFlipperPlugin();
42 | NetworkingModule.setCustomClientBuilder(
43 | new NetworkingModule.CustomClientBuilder() {
44 | @Override
45 | public void apply(OkHttpClient.Builder builder) {
46 | builder.addNetworkInterceptor(new FlipperOkhttpInterceptor(networkFlipperPlugin));
47 | }
48 | });
49 | client.addPlugin(networkFlipperPlugin);
50 | client.start();
51 |
52 | // Fresco Plugin needs to ensure that ImagePipelineFactory is initialized
53 | // Hence we run if after all native modules have been initialized
54 | ReactContext reactContext = reactInstanceManager.getCurrentReactContext();
55 | if (reactContext == null) {
56 | reactInstanceManager.addReactInstanceEventListener(
57 | new ReactInstanceEventListener() {
58 | @Override
59 | public void onReactContextInitialized(ReactContext reactContext) {
60 | reactInstanceManager.removeReactInstanceEventListener(this);
61 | reactContext.runOnNativeModulesQueueThread(
62 | new Runnable() {
63 | @Override
64 | public void run() {
65 | client.addPlugin(new FrescoFlipperPlugin());
66 | }
67 | });
68 | }
69 | });
70 | } else {
71 | client.addPlugin(new FrescoFlipperPlugin());
72 | }
73 | }
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/examples/demo/ios/Example/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
24 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/.github/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | Thanks for reading this guide and considering to contribute code to the project!
2 | We always welcome contributions to `react-native-app-auth`.
3 |
4 | This document will get you started on how to contribute and things you should know.
5 | So please give it a thorough read.
6 |
7 | This guide will help you to get started setting up the repository, and how to contribute
8 | a change. Please read it carefully.
9 |
10 | If you're reading this, please take a look at our [Code of Conduct](CODE_OF_CONDUCT.md)
11 | as well.
12 |
13 | ## How do I set up the project?
14 |
15 | First of all, you'll need to run `yarn install` to install all dependencies.
16 |
17 | All of the module's JavaScript code is located inside `index.js`, and all of the tests
18 | are located inside `index.spec.js`.
19 |
20 | You can run the tests using `yarn test`, which uses Jest.
21 |
22 | Please note that the project is set up to use our eslint rules.
23 | You can run the linter manually using `yarn lint`.
24 |
25 |
26 |
27 | ## Where is the native code?
28 |
29 | Since most of the code inside `index.js` is just an interface to the native code of this project
30 | you'll most likely be searching for those native modules instead.
31 |
32 | Those are located at:
33 |
34 | - `./android/src/main/java/com/reactlibrary/RNAppAuth{Module,Package}.java` for Android
35 | - `./ios/RNAppAuth.{m,h}` for iOS
36 |
37 | All changes to the behaviour of the Android native module must be replicated for iOS as well,
38 | and vice-versa.
39 | If you don't feel comfortable making changes to both, feel free to contribute
40 | (open a PR) for one of them first, and ask for help.
41 |
42 | ## How do I contribute code?
43 |
44 | 1. Search for something you'd like to change. This can be an open issue, or just a feature
45 | you'd like to implement. Make sure that no one else is already on it, so that you're not
46 | duplicating someone else's effort.
47 |
48 | 2. Fork the repository and then clone it, i.e. `git clone https://github.com/YOUR_NAME/react-native-app-auth.git`
49 |
50 | 3. Checkout a new branch with a descriptive name, e.g. `git checkout -b fix/issue-123`
51 |
52 | 4. Make your changes :sparkles:
53 |
54 | 5. Update the tests if necessary and make sure that the existing ones are still passing using `yarn test`
55 |
56 | 6. Make sure that your code is adhering to the linter's rules using `yarn lint`
57 |
58 | 7. Commit your changes with a short description, `git add -A && git commit -m 'Your meaningful and descriptive message'`
59 |
60 | 8. Push your new branch, `git push -u origin fix/issue-123`
61 |
62 | 9. Finally, open a pull request with a title and a short summary of what has been changed and why.
63 |
64 | 10. Wait for a maintainer to review it and make some changes as they're being recommended and as you see fit.
65 |
66 | 11. Get it merged and make cool a celebratory pose! :dancer:
67 |
68 | ### Using changesets
69 |
70 | Our official release path is to use automation to perform the actual publishing of our packages. The steps are to:
71 |
72 | 1. A human developer adds a changeset. Ideally this is as a part of a PR that will have a version impact on a package.
73 | 2. On merge of a PR our automation system opens a "Version Packages" PR.
74 | 3. On merging the "Version Packages" PR, the automation system publishes the packages.
75 |
76 | Here are more details:
77 |
78 | ### Add a changeset
79 |
80 | When you would like to add a changeset (which creates a file indicating the type of change), in your branch/PR issue this command:
81 |
82 | ```sh
83 | $ yarn changeset
84 | ```
85 |
86 | to produce an interactive menu. Navigate the packages with arrow keys and hit `` to select 1+ packages. Hit `` when done. Select semver versions for packages and add appropriate messages. From there, you'll be prompted to enter a summary of the change. Some tips for this summary:
87 |
88 | 1. Aim for a single line, 1+ sentences as appropriate.
89 | 2. Include issue links in GH format (e.g. `#123`).
90 | 3. You don't need to reference the current pull request or whatnot, as that will be added later automatically.
91 |
92 | After this, you'll see a new uncommitted file in `.changesets` like:
93 |
94 | ```sh
95 | $ git status
96 | # ....
97 | Untracked files:
98 | (use "git add ..." to include in what will be committed)
99 | .changeset/flimsy-pandas-marry.md
100 | ```
101 |
102 | Review the file, make any necessary adjustments, and commit it to source. When we eventually do a package release, the changeset notes and version will be incorporated!
103 |
104 | ### Creating versions
105 |
106 | On a merge of a feature PR, the changesets GitHub action will open a new PR titled `"Version Packages"`. This PR is automatically kept up to date with additional PRs with changesets. So, if you're not ready to publish yet, just keep merging feature PRs and then merge the version packages PR later.
107 |
108 | ### Publishing packages
109 |
110 | On the merge of a version packages PR, the changesets GitHub action will publish the packages to npm.
111 |
--------------------------------------------------------------------------------
/packages/react-native-app-auth/android/src/main/java/com/rnappauth/utils/TokenResponseFactory.java:
--------------------------------------------------------------------------------
1 | package com.rnappauth.utils;
2 |
3 | import android.text.TextUtils;
4 |
5 | import com.facebook.react.bridge.Arguments;
6 | import com.facebook.react.bridge.WritableArray;
7 | import com.facebook.react.bridge.WritableMap;
8 |
9 | import net.openid.appauth.AuthorizationResponse;
10 | import net.openid.appauth.TokenResponse;
11 |
12 | public final class TokenResponseFactory {
13 |
14 | private static final WritableArray createScopeArray(String scope) {
15 | WritableArray scopeArray = Arguments.createArray();
16 | if (!TextUtils.isEmpty(scope)) {
17 | String[] scopesArray = scope.split(" ");
18 |
19 | for( int i = 0; i < scopesArray.length; i++)
20 | {
21 | scopeArray.pushString(scopesArray[i]);
22 | }
23 | }
24 |
25 | return scopeArray;
26 | }
27 |
28 |
29 | /*
30 | * Read raw token response into a React Native map to be passed down the bridge
31 | */
32 | public static final WritableMap tokenResponseToMap(TokenResponse response) {
33 | WritableMap map = Arguments.createMap();
34 |
35 | map.putString("accessToken", response.accessToken);
36 | map.putMap("additionalParameters", MapUtil.createAdditionalParametersMap(response.additionalParameters));
37 | map.putString("idToken", response.idToken);
38 | map.putString("refreshToken", response.refreshToken);
39 | map.putString("tokenType", response.tokenType);
40 |
41 | if (response.accessTokenExpirationTime != null) {
42 | map.putString("accessTokenExpirationDate", DateUtil.formatTimestamp(response.accessTokenExpirationTime));
43 | }
44 |
45 | return map;
46 | }
47 |
48 | /*
49 | * Read raw token response into a React Native map to be passed down the bridge
50 | */
51 | public static final WritableMap tokenResponseToMap(TokenResponse response, AuthorizationResponse authResponse) {
52 | WritableMap map = Arguments.createMap();
53 |
54 | map.putString("accessToken", response.accessToken);
55 | map.putMap("authorizeAdditionalParameters", MapUtil.createAdditionalParametersMap(authResponse.additionalParameters));
56 | map.putMap("tokenAdditionalParameters", MapUtil.createAdditionalParametersMap(response.additionalParameters));
57 | map.putString("idToken", response.idToken);
58 | map.putString("refreshToken", response.refreshToken);
59 | map.putString("tokenType", response.tokenType);
60 | map.putArray("scopes", createScopeArray(authResponse.scope));
61 |
62 | if (response.accessTokenExpirationTime != null) {
63 | map.putString("accessTokenExpirationDate", DateUtil.formatTimestamp(response.accessTokenExpirationTime));
64 | }
65 |
66 |
67 | return map;
68 | }
69 |
70 |
71 | /*
72 | * Read raw authorization into a React Native map to be passed down the bridge
73 | */
74 | public static final WritableMap authorizationResponseToMap(AuthorizationResponse authResponse) {
75 | WritableMap map = Arguments.createMap();
76 | map.putString("authorizationCode", authResponse.authorizationCode);
77 | map.putString("accessToken", authResponse.accessToken);
78 | map.putMap("additionalParameters", MapUtil.createAdditionalParametersMap(authResponse.additionalParameters));
79 | map.putString("idToken", authResponse.idToken);
80 | map.putString("tokenType", authResponse.tokenType);
81 | map.putArray("scopes", createScopeArray(authResponse.scope));
82 |
83 | if (authResponse.accessTokenExpirationTime != null) {
84 | map.putString("accessTokenExpirationTime", DateUtil.formatTimestamp(authResponse.accessTokenExpirationTime));
85 | }
86 |
87 | return map;
88 | }
89 |
90 | /*
91 | * Read raw authorization into a React Native map with codeVerifier value added if present to be passed down the bridge
92 | */
93 | public static final WritableMap authorizationCodeResponseToMap(AuthorizationResponse authResponse, String codeVerifier) {
94 | WritableMap map = Arguments.createMap();
95 | map.putString("authorizationCode", authResponse.authorizationCode);
96 | map.putString("accessToken", authResponse.accessToken);
97 | map.putMap("additionalParameters", MapUtil.createAdditionalParametersMap(authResponse.additionalParameters));
98 | map.putString("idToken", authResponse.idToken);
99 | map.putString("tokenType", authResponse.tokenType);
100 | map.putArray("scopes", createScopeArray(authResponse.scope));
101 |
102 | if (authResponse.accessTokenExpirationTime != null) {
103 | map.putString("accessTokenExpirationTime", DateUtil.formatTimestamp(authResponse.accessTokenExpirationTime));
104 | }
105 |
106 | if (!TextUtils.isEmpty(codeVerifier)) {
107 | map.putString("codeVerifier", codeVerifier);
108 | }
109 |
110 | return map;
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/packages/react-native-app-auth/android/src/main/java/com/rnappauth/utils/MapUtil.java:
--------------------------------------------------------------------------------
1 | package com.rnappauth.utils;
2 |
3 | import android.util.Log;
4 |
5 | import androidx.annotation.Nullable;
6 |
7 | import com.facebook.react.bridge.Arguments;
8 | import com.facebook.react.bridge.ReadableMap;
9 | import com.facebook.react.bridge.ReadableMapKeySetIterator;
10 | import com.facebook.react.bridge.WritableArray;
11 | import com.facebook.react.bridge.WritableMap;
12 | import com.facebook.react.bridge.WritableNativeArray;
13 | import com.facebook.react.bridge.WritableNativeMap;
14 |
15 | import org.json.JSONArray;
16 | import org.json.JSONException;
17 | import org.json.JSONObject;
18 |
19 | import java.util.HashMap;
20 | import java.util.Iterator;
21 | import java.util.Map;
22 |
23 | public class MapUtil {
24 |
25 | public static HashMap readableMapToHashMap(@Nullable ReadableMap readableMap) {
26 |
27 | HashMap hashMap = new HashMap<>();
28 | if (readableMap != null) {
29 | ReadableMapKeySetIterator iterator = readableMap.keySetIterator();
30 | while (iterator.hasNextKey()) {
31 | String nextKey = iterator.nextKey();
32 | hashMap.put(nextKey, readableMap.getString(nextKey));
33 | }
34 | }
35 |
36 | return hashMap;
37 | }
38 |
39 | public static final WritableMap createAdditionalParametersMap(Map additionalParameters) {
40 | WritableMap additionalParametersMap = Arguments.createMap();
41 |
42 | if (!additionalParameters.isEmpty()) {
43 |
44 | Iterator iterator = additionalParameters.keySet().iterator();
45 |
46 | while(iterator.hasNext()) {
47 | String key = iterator.next();
48 | String value = additionalParameters.get(key);
49 | // Try to parse to JSON
50 | try {
51 | JSONObject jsonObject = new JSONObject(value);
52 | WritableMap json = convertJsonToMap(jsonObject);
53 | additionalParametersMap.putMap(key, json);
54 | continue;
55 | } catch (JSONException ignored) {
56 |
57 | }
58 | additionalParametersMap.putString(key, additionalParameters.get(key));
59 | }
60 | }
61 |
62 | return additionalParametersMap;
63 | }
64 |
65 | private static WritableMap convertJsonToMap(JSONObject jsonObject) throws JSONException {
66 | WritableMap map = new WritableNativeMap();
67 |
68 | Iterator iterator = jsonObject.keys();
69 | while (iterator.hasNext()) {
70 | String key = iterator.next();
71 | Object value = jsonObject.get(key);
72 | if (value instanceof JSONObject) {
73 | map.putMap(key, convertJsonToMap((JSONObject) value));
74 | } else if (value instanceof JSONArray) {
75 | map.putArray(key, convertJsonToArray((JSONArray) value));
76 | } else if (value instanceof Boolean) {
77 | map.putBoolean(key, (Boolean) value);
78 | } else if (value instanceof Integer) {
79 | map.putInt(key, (Integer) value);
80 | } else if (value instanceof Double) {
81 | map.putDouble(key, (Double) value);
82 | } else if (value instanceof Float) {
83 | map.putDouble(key, ((Float) value).doubleValue());
84 | } else if (value instanceof Long) {
85 | map.putDouble(key, ((Long) value).doubleValue());
86 | } else if (value instanceof String) {
87 | map.putString(key, (String) value);
88 | } else {
89 | map.putString(key, value.toString());
90 | }
91 | }
92 | return map;
93 | }
94 |
95 | private static WritableArray convertJsonToArray(JSONArray jsonArray) throws JSONException {
96 | WritableArray array = new WritableNativeArray();
97 |
98 | for (int i = 0; i < jsonArray.length(); i++) {
99 | Object value = jsonArray.get(i);
100 | if (value instanceof JSONObject) {
101 | array.pushMap(convertJsonToMap((JSONObject) value));
102 | } else if (value instanceof JSONArray) {
103 | array.pushArray(convertJsonToArray((JSONArray) value));
104 | } else if (value instanceof Boolean) {
105 | array.pushBoolean((Boolean) value);
106 | } else if (value instanceof Integer) {
107 | array.pushInt((Integer) value);
108 | } else if (value instanceof Double) {
109 | array.pushDouble((Double) value);
110 | } else if (value instanceof Float) {
111 | array.pushDouble(((Float) value).doubleValue());
112 | } else if (value instanceof Long) {
113 | array.pushDouble(((Long) value).doubleValue());
114 | } else if (value instanceof String) {
115 | array.pushString((String) value);
116 | } else {
117 | array.pushString(value.toString());
118 | }
119 | }
120 | return array;
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/packages/react-native-app-auth/android/src/main/java/com/rnappauth/utils/UnsafeConnectionBuilder.java:
--------------------------------------------------------------------------------
1 | package com.rnappauth.utils;
2 |
3 | /*
4 | * Copyright 2016 The AppAuth for Android Authors. All Rights Reserved.
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
7 | * in compliance with the License. You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software distributed under the
12 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
13 | * express or implied. See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 |
18 | import android.annotation.SuppressLint;
19 | import android.net.Uri;
20 | import androidx.annotation.NonNull;
21 | import androidx.annotation.Nullable;
22 | import android.util.Log;
23 |
24 | import net.openid.appauth.Preconditions;
25 | import net.openid.appauth.connectivity.ConnectionBuilder;
26 |
27 | import java.io.IOException;
28 | import java.net.HttpURLConnection;
29 | import java.net.URL;
30 | import java.security.KeyManagementException;
31 | import java.security.NoSuchAlgorithmException;
32 | import java.security.cert.X509Certificate;
33 | import java.util.concurrent.TimeUnit;
34 |
35 | import javax.net.ssl.HostnameVerifier;
36 | import javax.net.ssl.HttpsURLConnection;
37 | import javax.net.ssl.SSLContext;
38 | import javax.net.ssl.SSLSession;
39 | import javax.net.ssl.TrustManager;
40 | import javax.net.ssl.X509TrustManager;
41 |
42 | /**
43 | * An implementation of {@link ConnectionBuilder} that permits connecting to http
44 | * links, and ignores certificates for https connections. *THIS SHOULD NOT BE USED IN PRODUCTION
45 | * CODE*. It is intended to facilitate easier testing of AppAuth against development servers
46 | * only.
47 | */
48 | public final class UnsafeConnectionBuilder implements ConnectionBuilder {
49 |
50 | public static final UnsafeConnectionBuilder INSTANCE = new UnsafeConnectionBuilder();
51 |
52 | private static final String TAG = "ConnBuilder";
53 |
54 | private static final int CONNECTION_TIMEOUT_MS = (int) TimeUnit.SECONDS.toMillis(15);
55 | private static final int READ_TIMEOUT_MS = (int) TimeUnit.SECONDS.toMillis(10);
56 |
57 | private static final String HTTP = "http";
58 | private static final String HTTPS = "https";
59 |
60 | @SuppressLint("TrustAllX509TrustManager")
61 | private static final TrustManager[] ANY_CERT_MANAGER = new TrustManager[] {
62 | new X509TrustManager() {
63 | public X509Certificate[] getAcceptedIssuers() {
64 | return null;
65 | }
66 |
67 | public void checkClientTrusted(X509Certificate[] certs, String authType) {}
68 |
69 | public void checkServerTrusted(X509Certificate[] certs, String authType) {}
70 | }
71 | };
72 |
73 | @SuppressLint("BadHostnameVerifier")
74 | private static final HostnameVerifier ANY_HOSTNAME_VERIFIER = new HostnameVerifier() {
75 | public boolean verify(String hostname, SSLSession session) {
76 | return true;
77 | }
78 | };
79 |
80 | @Nullable
81 | private static final SSLContext TRUSTING_CONTEXT;
82 |
83 | static {
84 | SSLContext context;
85 | try {
86 | context = SSLContext.getInstance("SSL");
87 | } catch (NoSuchAlgorithmException e) {
88 | Log.e("ConnBuilder", "Unable to acquire SSL context");
89 | context = null;
90 | }
91 |
92 | SSLContext initializedContext = null;
93 | if (context != null) {
94 | try {
95 | context.init(null, ANY_CERT_MANAGER, new java.security.SecureRandom());
96 | initializedContext = context;
97 | } catch (KeyManagementException e) {
98 | Log.e(TAG, "Failed to initialize trusting SSL context");
99 | }
100 | }
101 |
102 | TRUSTING_CONTEXT = initializedContext;
103 | }
104 |
105 | private UnsafeConnectionBuilder() {
106 | // no need to construct new instances
107 | }
108 |
109 | @NonNull
110 | @Override
111 | public HttpURLConnection openConnection(@NonNull Uri uri) throws IOException {
112 | Preconditions.checkNotNull(uri, "url must not be null");
113 | Preconditions.checkArgument(HTTP.equals(uri.getScheme()) || HTTPS.equals(uri.getScheme()),
114 | "scheme or uri must be http or https");
115 | HttpURLConnection conn = (HttpURLConnection) new URL(uri.toString()).openConnection();
116 | conn.setConnectTimeout(CONNECTION_TIMEOUT_MS);
117 | conn.setReadTimeout(READ_TIMEOUT_MS);
118 | conn.setInstanceFollowRedirects(false);
119 |
120 | if (conn instanceof HttpsURLConnection && TRUSTING_CONTEXT != null) {
121 | HttpsURLConnection httpsConn = (HttpsURLConnection) conn;
122 | httpsConn.setSSLSocketFactory(TRUSTING_CONTEXT.getSocketFactory());
123 | httpsConn.setHostnameVerifier(ANY_HOSTNAME_VERIFIER);
124 | }
125 |
126 | return conn;
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/examples/demo/android/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: "com.android.application"
2 | apply plugin: "com.facebook.react"
3 |
4 | /**
5 | * This is the configuration block to customize your React Native Android app.
6 | * By default you don't need to apply any configuration, just uncomment the lines you need.
7 | */
8 | react {
9 | /* Folders */
10 | // The root of your project, i.e. where "package.json" lives. Default is '..'
11 | // root = file("../")
12 | // The folder where the react-native NPM package is. Default is ../node_modules/react-native
13 | // reactNativeDir = file("../node_modules/react-native")
14 | // The folder where the react-native Codegen package is. Default is ../node_modules/@react-native/codegen
15 | // codegenDir = file("../node_modules/@react-native/codegen")
16 | // The cli.js file which is the React Native CLI entrypoint. Default is ../node_modules/react-native/cli.js
17 | // cliFile = file("../node_modules/react-native/cli.js")
18 |
19 | /* Variants */
20 | // The list of variants to that are debuggable. For those we're going to
21 | // skip the bundling of the JS bundle and the assets. By default is just 'debug'.
22 | // If you add flavors like lite, prod, etc. you'll have to list your debuggableVariants.
23 | // debuggableVariants = ["liteDebug", "prodDebug"]
24 |
25 | /* Bundling */
26 | // A list containing the node command and its flags. Default is just 'node'.
27 | // nodeExecutableAndArgs = ["node"]
28 | //
29 | // The command to run when bundling. By default is 'bundle'
30 | // bundleCommand = "ram-bundle"
31 | //
32 | // The path to the CLI configuration file. Default is empty.
33 | // bundleConfig = file(../rn-cli.config.js)
34 | //
35 | // The name of the generated asset file containing your JS bundle
36 | // bundleAssetName = "MyApplication.android.bundle"
37 | //
38 | // The entry file for bundle generation. Default is 'index.android.js' or 'index.js'
39 | // entryFile = file("../js/MyApplication.android.js")
40 | //
41 | // A list of extra flags to pass to the 'bundle' commands.
42 | // See https://github.com/react-native-community/cli/blob/main/docs/commands.md#bundle
43 | // extraPackagerArgs = []
44 |
45 | /* Hermes Commands */
46 | // The hermes compiler command to run. By default it is 'hermesc'
47 | // hermesCommand = "$rootDir/my-custom-hermesc/bin/hermesc"
48 | //
49 | // The list of flags to pass to the Hermes compiler. By default is "-O", "-output-source-map"
50 | // hermesFlags = ["-O", "-output-source-map"]
51 | }
52 |
53 | /**
54 | * Set this to true to Run Proguard on Release builds to minify the Java bytecode.
55 | */
56 | def enableProguardInReleaseBuilds = false
57 |
58 | /**
59 | * The preferred build flavor of JavaScriptCore (JSC)
60 | *
61 | * For example, to use the international variant, you can use:
62 | * `def jscFlavor = 'org.webkit:android-jsc-intl:+'`
63 | *
64 | * The international variant includes ICU i18n library and necessary data
65 | * allowing to use e.g. `Date.toLocaleString` and `String.localeCompare` that
66 | * give correct results when using with locales other than en-US. Note that
67 | * this variant is about 6MiB larger per architecture than default.
68 | */
69 | def jscFlavor = 'org.webkit:android-jsc:+'
70 |
71 | android {
72 | ndkVersion rootProject.ext.ndkVersion
73 |
74 | compileSdkVersion rootProject.ext.compileSdkVersion
75 |
76 | namespace "com.example"
77 | defaultConfig {
78 | applicationId "com.example"
79 | minSdkVersion rootProject.ext.minSdkVersion
80 | targetSdkVersion rootProject.ext.targetSdkVersion
81 | versionCode 1
82 | versionName "1.0"
83 | }
84 | signingConfigs {
85 | debug {
86 | storeFile file('debug.keystore')
87 | storePassword 'android'
88 | keyAlias 'androiddebugkey'
89 | keyPassword 'android'
90 | }
91 | }
92 | buildTypes {
93 | debug {
94 | signingConfig signingConfigs.debug
95 | }
96 | release {
97 | // Caution! In production, you need to generate your own keystore file.
98 | // see https://reactnative.dev/docs/signed-apk-android.
99 | signingConfig signingConfigs.debug
100 | minifyEnabled enableProguardInReleaseBuilds
101 | proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
102 | }
103 | }
104 | }
105 |
106 | dependencies {
107 | // The version of react-native is set by the React Native Gradle Plugin
108 | implementation("com.facebook.react:react-android")
109 |
110 | debugImplementation("com.facebook.flipper:flipper:${FLIPPER_VERSION}")
111 | debugImplementation("com.facebook.flipper:flipper-network-plugin:${FLIPPER_VERSION}") {
112 | exclude group:'com.squareup.okhttp3', module:'okhttp'
113 | }
114 |
115 | debugImplementation("com.facebook.flipper:flipper-fresco-plugin:${FLIPPER_VERSION}")
116 | if (hermesEnabled.toBoolean()) {
117 | implementation("com.facebook.react:hermes-android")
118 | } else {
119 | implementation jscFlavor
120 | }
121 | }
122 |
123 | apply from: file("../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesAppBuildGradle(project)
124 |
--------------------------------------------------------------------------------
/examples/demo/App.js:
--------------------------------------------------------------------------------
1 | import React, {useState, useCallback, useMemo} from 'react';
2 | import {Alert} from 'react-native';
3 | import {
4 | authorize,
5 | refresh,
6 | revoke,
7 | prefetchConfiguration,
8 | } from 'react-native-app-auth';
9 | import {
10 | Page,
11 | Button,
12 | ButtonContainer,
13 | Form,
14 | FormLabel,
15 | FormValue,
16 | Heading,
17 | } from './components';
18 |
19 | const configs = {
20 | identityserver: {
21 | issuer: 'https://demo.duendesoftware.com',
22 | clientId: 'interactive.public',
23 | redirectUrl: 'io.identityserver.demo:/oauthredirect',
24 | additionalParameters: {},
25 | scopes: ['openid', 'profile', 'email', 'offline_access'],
26 |
27 | // serviceConfiguration: {
28 | // authorizationEndpoint: 'https://demo.duendesoftware.com/connect/authorize',
29 | // tokenEndpoint: 'https://demo.duendesoftware.com/connect/token',
30 | // revocationEndpoint: 'https://demo.duendesoftware.com/connect/revoke'
31 | // }
32 | },
33 | auth0: {
34 | issuer: 'https://rnaa-demo.eu.auth0.com',
35 | clientId: 'VtXdAoGFcYzZ3IJaNy4UIS5RNHhdbKbU',
36 | redirectUrl: 'rnaa-demo://oauthredirect',
37 | additionalParameters: {},
38 | scopes: ['openid', 'profile', 'email', 'offline_access'],
39 |
40 | // serviceConfiguration: {
41 | // authorizationEndpoint: 'https://samples.auth0.com/authorize',
42 | // tokenEndpoint: 'https://samples.auth0.com/oauth/token',
43 | // revocationEndpoint: 'https://samples.auth0.com/oauth/revoke'
44 | // }
45 | },
46 | };
47 |
48 | const defaultAuthState = {
49 | hasLoggedInOnce: false,
50 | provider: '',
51 | accessToken: '',
52 | accessTokenExpirationDate: '',
53 | refreshToken: '',
54 | };
55 |
56 | const App = () => {
57 | const [authState, setAuthState] = useState(defaultAuthState);
58 | React.useEffect(() => {
59 | prefetchConfiguration({
60 | warmAndPrefetchChrome: true,
61 | connectionTimeoutSeconds: 5,
62 | ...configs.auth0,
63 | });
64 | }, []);
65 |
66 | const handleAuthorize = useCallback(async provider => {
67 | try {
68 | const config = configs[provider];
69 | const newAuthState = await authorize({
70 | ...config,
71 | connectionTimeoutSeconds: 5,
72 | iosPrefersEphemeralSession: true,
73 | });
74 |
75 | setAuthState({
76 | hasLoggedInOnce: true,
77 | provider: provider,
78 | ...newAuthState,
79 | });
80 | } catch (error) {
81 | Alert.alert('Failed to log in', error.message);
82 | }
83 | }, []);
84 |
85 | const handleRefresh = useCallback(async () => {
86 | try {
87 | const config = configs[authState.provider];
88 | const newAuthState = await refresh(config, {
89 | refreshToken: authState.refreshToken,
90 | });
91 |
92 | setAuthState(current => ({
93 | ...current,
94 | ...newAuthState,
95 | refreshToken: newAuthState.refreshToken || current.refreshToken,
96 | }));
97 | } catch (error) {
98 | Alert.alert('Failed to refresh token', error.message);
99 | }
100 | }, [authState]);
101 |
102 | const handleRevoke = useCallback(async () => {
103 | try {
104 | const config = configs[authState.provider];
105 | await revoke(config, {
106 | tokenToRevoke: authState.accessToken,
107 | sendClientId: true,
108 | });
109 |
110 | setAuthState({
111 | provider: '',
112 | accessToken: '',
113 | accessTokenExpirationDate: '',
114 | refreshToken: '',
115 | });
116 | } catch (error) {
117 | Alert.alert('Failed to revoke token', error.message);
118 | }
119 | }, [authState]);
120 |
121 | const showRevoke = useMemo(() => {
122 | if (authState.accessToken) {
123 | const config = configs[authState.provider];
124 | if (config.issuer || config.serviceConfiguration.revocationEndpoint) {
125 | return true;
126 | }
127 | }
128 | return false;
129 | }, [authState]);
130 |
131 | return (
132 |
133 | {authState.accessToken ? (
134 |
144 | ) : (
145 |
146 | {authState.hasLoggedInOnce ? 'Goodbye.' : 'Hello, stranger.'}
147 |
148 | )}
149 |
150 |
151 | {!authState.accessToken ? (
152 | <>
153 |
172 |
173 | );
174 | };
175 |
176 | export default App;
177 |
--------------------------------------------------------------------------------
/docs/docs/usage/config.md:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar_position: 1
3 | ---
4 |
5 | # Configuration
6 |
7 | This is your configuration object for the client. The config is passed into each of the methods
8 | with optional overrides.
9 |
10 | ```js
11 | const config = {
12 | issuer: '',
13 | clientId: '',
14 | redirectUrl: '',
15 | scopes: [''],
16 | };
17 |
18 | const result = await authorize(config);
19 | ```
20 |
21 | See specific example [configurations for your provider](/docs/category/providers)
22 |
23 | ## API
24 |
25 | - **issuer** - (`string`) base URI of the authentication server. If no `serviceConfiguration` (below) is provided, issuer is a mandatory field, so that the configuration can be fetched from the issuer's [OIDC discovery endpoint](https://openid.net/specs/openid-connect-discovery-1_0.html).
26 | - **serviceConfiguration** - (`object`) you may manually configure token exchange endpoints in cases where the issuer does not support the OIDC discovery protocol, or simply to avoid an additional round trip to fetch the configuration. If no `issuer` (above) is provided, the service configuration is mandatory.
27 | - **authorizationEndpoint** - (`string`) _REQUIRED_ fully formed url to the OAuth authorization endpoint
28 | - **tokenEndpoint** - (`string`) _REQUIRED_ fully formed url to the OAuth token exchange endpoint
29 | - **revocationEndpoint** - (`string`) fully formed url to the OAuth token revocation endpoint. If you want to be able to revoke a token and no `issuer` is specified, this field is mandatory.
30 | - **registrationEndpoint** - (`string`) fully formed url to your OAuth/OpenID Connect registration endpoint. Only necessary for servers that require client registration.
31 | - **endSessionEndpoint** - (`string`) fully formed url to your OpenID Connect end session endpoint. If you want to be able to end a user's session and no `issuer` is specified, this field is mandatory.
32 | - **clientId** - (`string`) _REQUIRED_ your client id on the auth server
33 | - **clientSecret** - (`string`) client secret to pass to token exchange requests. :warning: Read more about [client secrets](/docs/client-secrets)
34 | - **redirectUrl** - (`string`) _REQUIRED_ the url that links back to your app with the auth code. Depending on your [provider](/docs/category/providers), you may find that you need to add a trailing slash to your redirect URL.
35 | - **scopes** - (`array`) the scopes for your token, e.g. `['email', 'offline_access']`.
36 | - **additionalParameters** - (`object`) additional parameters that will be passed in the authorization request.
37 | Must be string values! E.g. setting `additionalParameters: { hello: 'world', foo: 'bar' }` would add
38 | `hello=world&foo=bar` to the authorization request.
39 | - **clientAuthMethod** - (`string`) _ANDROID_ Client Authentication Method. Can be either `basic` (default) for [Basic Authentication](https://github.com/openid/AppAuth-Android/blob/master/library/java/net/openid/appauth/ClientSecretBasic.java) or `post` for [HTTP POST body Authentication](https://github.com/openid/AppAuth-Android/blob/master/library/java/net/openid/appauth/ClientSecretPost.java)
40 | - **dangerouslyAllowInsecureHttpRequests** - (`boolean`) _ANDROID_ whether to allow requests over plain HTTP or with self-signed SSL certificates. :warning: Can be useful for testing against local server, _should not be used in production._ This setting has no effect on iOS; to enable insecure HTTP requests, add a [NSExceptionAllowsInsecureHTTPLoads exception](https://cocoacasts.com/how-to-add-app-transport-security-exception-domains) to your App Transport Security settings.
41 | - **customHeaders** - (`object`) _ANDROID_ you can specify custom headers to pass during authorize request and/or token request.
42 | - **authorize** - (`{ [key: string]: value }`) headers to be passed during authorization request.
43 | - **token** - (`{ [key: string]: value }`) headers to be passed during token retrieval request.
44 | - **register** - (`{ [key: string]: value }`) headers to be passed during registration request.
45 | - **additionalHeaders** - (`{ [key: string]: value }`) _IOS_ you can specify additional headers to be passed for all authorize, refresh, and register requests.
46 | - **useNonce** - (`boolean`) (default: true) optionally allows not sending the nonce parameter, to support non-compliant providers. To specify custom nonce, provide it in `additionalParameters` under the `nonce` key.
47 | - **usePKCE** - (`boolean`) (default: true) optionally allows not sending the code_challenge parameter and skipping PKCE code verification, to support non-compliant providers.
48 | - **skipCodeExchange** - (`boolean`) (default: false) just return the authorization response, instead of automatically exchanging the authorization code. This is useful if this exchange needs to be done manually (not client-side)
49 | - **iosCustomBrowser** - (`string`) (default: undefined) _IOS_ override the used browser for authorization, used to open an external browser. If no value is provided, the `ASWebAuthenticationSession` or `SFSafariViewController` are used by the `AppAuth-iOS` library.
50 | - **iosPrefersEphemeralSession** - (`boolean`) (default: `false`) _IOS_ indicates whether the session should ask the browser for a private authentication session.
51 | - **androidAllowCustomBrowsers** - (`string[]`) (default: undefined) _ANDROID_ override the used browser for authorization. If no value is provided, all browsers are allowed.
52 | - **androidTrustedWebActivity** - (`boolean`) (default: `false`) _ANDROID_ Use [`EXTRA_LAUNCH_AS_TRUSTED_WEB_ACTIVITY`](https://developer.chrome.com/docs/android/trusted-web-activity/) when opening web view.
53 | - **connectionTimeoutSeconds** - (`number`) configure the request timeout interval in seconds. This must be a positive number. The default values are 60 seconds on iOS and 15 seconds on Android.
54 |
--------------------------------------------------------------------------------
/packages/react-native-app-auth/index.d.ts:
--------------------------------------------------------------------------------
1 | export interface ServiceConfiguration {
2 | authorizationEndpoint: string;
3 | tokenEndpoint: string;
4 | revocationEndpoint?: string;
5 | registrationEndpoint?: string;
6 | endSessionEndpoint?: string;
7 | }
8 |
9 | export type BaseConfiguration =
10 | | {
11 | issuer?: string;
12 | serviceConfiguration: ServiceConfiguration;
13 | }
14 | | {
15 | issuer: string;
16 | serviceConfiguration?: ServiceConfiguration;
17 | };
18 |
19 | type CustomHeaders = {
20 | authorize?: Record;
21 | token?: Record;
22 | register?: Record;
23 | };
24 |
25 | type AdditionalHeaders = Record;
26 |
27 | interface BuiltInRegistrationParameters {
28 | client_name?: string;
29 | logo_uri?: string;
30 | client_uri?: string;
31 | policy_uri?: string;
32 | tos_uri?: string;
33 | }
34 |
35 | export type RegistrationConfiguration = BaseConfiguration & {
36 | redirectUrls: string[];
37 | responseTypes?: string[];
38 | grantTypes?: string[];
39 | subjectType?: string;
40 | tokenEndpointAuthMethod?: string;
41 | additionalParameters?: BuiltInRegistrationParameters & { [name: string]: string };
42 | dangerouslyAllowInsecureHttpRequests?: boolean;
43 | customHeaders?: CustomHeaders;
44 | additionalHeaders?: AdditionalHeaders;
45 | };
46 |
47 | export interface RegistrationResponse {
48 | clientId: string;
49 | additionalParameters?: { [name: string]: string };
50 | clientIdIssuedAt?: string;
51 | clientSecret?: string;
52 | clientSecretExpiresAt?: string;
53 | registrationAccessToken?: string;
54 | registrationClientUri?: string;
55 | tokenEndpointAuthMethod?: string;
56 | }
57 |
58 | interface BuiltInParameters {
59 | display?: 'page' | 'popup' | 'touch' | 'wap';
60 | login_prompt?: string;
61 | prompt?: 'consent' | 'login' | 'none' | 'select_account';
62 | }
63 |
64 | export type BaseAuthConfiguration = BaseConfiguration & {
65 | clientId: string;
66 | };
67 |
68 | export type AuthConfiguration = BaseAuthConfiguration & {
69 | clientSecret?: string;
70 | scopes: string[];
71 | redirectUrl: string;
72 | additionalParameters?: BuiltInParameters & { [name: string]: string };
73 | clientAuthMethod?: 'basic' | 'post';
74 | dangerouslyAllowInsecureHttpRequests?: boolean;
75 | customHeaders?: CustomHeaders;
76 | additionalHeaders?: AdditionalHeaders;
77 | connectionTimeoutSeconds?: number;
78 | useNonce?: boolean;
79 | usePKCE?: boolean;
80 | warmAndPrefetchChrome?: boolean;
81 | skipCodeExchange?: boolean;
82 | iosCustomBrowser?: 'safari' | 'chrome' | 'opera' | 'firefox';
83 | androidAllowCustomBrowsers?: (
84 | | 'chrome'
85 | | 'chromeCustomTab'
86 | | 'firefox'
87 | | 'firefoxCustomTab'
88 | | 'samsung'
89 | | 'samsungCustomTab'
90 | )[];
91 | androidTrustedWebActivity?: boolean;
92 | iosPrefersEphemeralSession?: boolean;
93 | };
94 |
95 | export type EndSessionConfiguration = BaseAuthConfiguration & {
96 | additionalParameters?: { [name: string]: string };
97 | dangerouslyAllowInsecureHttpRequests?: boolean;
98 | iosPrefersEphemeralSession?: boolean;
99 | };
100 |
101 | export interface AuthorizeResult {
102 | accessToken: string;
103 | accessTokenExpirationDate: string;
104 | authorizeAdditionalParameters?: { [name: string]: string };
105 | tokenAdditionalParameters?: { [name: string]: string };
106 | idToken: string;
107 | refreshToken: string;
108 | tokenType: string;
109 | scopes: string[];
110 | authorizationCode: string;
111 | codeVerifier?: string;
112 | }
113 |
114 | export interface RefreshResult {
115 | accessToken: string;
116 | accessTokenExpirationDate: string;
117 | additionalParameters?: { [name: string]: string };
118 | idToken: string;
119 | refreshToken: string | null;
120 | tokenType: string;
121 | }
122 |
123 | export interface RevokeConfiguration {
124 | tokenToRevoke: string;
125 | sendClientId?: boolean;
126 | includeBasicAuth?: boolean;
127 | }
128 |
129 | export interface RefreshConfiguration {
130 | refreshToken: string;
131 | }
132 |
133 | export interface LogoutConfiguration {
134 | idToken: string;
135 | postLogoutRedirectUrl: string;
136 | }
137 |
138 | export interface EndSessionResult {
139 | idTokenHint: string;
140 | postLogoutRedirectUri: string;
141 | state: string;
142 | }
143 |
144 | export function prefetchConfiguration(config: AuthConfiguration): Promise;
145 |
146 | export function register(config: RegistrationConfiguration): Promise;
147 |
148 | export function authorize(config: AuthConfiguration): Promise;
149 |
150 | export function refresh(
151 | config: AuthConfiguration,
152 | refreshConfig: RefreshConfiguration
153 | ): Promise;
154 |
155 | export function revoke(
156 | config: BaseAuthConfiguration,
157 | revokeConfig: RevokeConfiguration
158 | ): Promise;
159 |
160 | export function logout(
161 | config: EndSessionConfiguration,
162 | logoutConfig: LogoutConfiguration
163 | ): Promise;
164 |
165 | // https://tools.ietf.org/html/rfc6749#section-4.1.2.1
166 | type OAuthAuthorizationErrorCode =
167 | | 'unauthorized_client'
168 | | 'access_denied'
169 | | 'unsupported_response_type'
170 | | 'invalid_scope'
171 | | 'server_error'
172 | | 'temporarily_unavailable';
173 | // https://tools.ietf.org/html/rfc6749#section-5.2
174 | type OAuthTokenErrorCode =
175 | | 'invalid_request'
176 | | 'invalid_client'
177 | | 'invalid_grant'
178 | | 'unauthorized_client'
179 | | 'unsupported_grant_type'
180 | | 'invalid_scope';
181 | // https://openid.net/specs/openid-connect-registration-1_0.html#RegistrationError
182 | type OICRegistrationErrorCode = 'invalid_redirect_uri' | 'invalid_client_metadata';
183 | type AppAuthErrorCode =
184 | | 'service_configuration_fetch_error'
185 | | 'authentication_failed'
186 | | 'token_refresh_failed'
187 | | 'token_exchange_failed'
188 | | 'registration_failed'
189 | | 'browser_not_found'
190 | | 'end_session_failed'
191 | | 'authentication_error'
192 | | 'run_time_exception';
193 |
194 | type ErrorCode =
195 | | OAuthAuthorizationErrorCode
196 | | OAuthTokenErrorCode
197 | | OICRegistrationErrorCode
198 | | AppAuthErrorCode;
199 |
200 | export interface AppAuthError extends Error {
201 | code: ErrorCode;
202 | }
203 |
--------------------------------------------------------------------------------