1import { H2, H4 } from '@expo/html-elements';
2import * as AuthSession from 'expo-auth-session';
3import { useAuthRequest } from 'expo-auth-session';
4import * as FacebookAuthSession from 'expo-auth-session/providers/facebook';
5import * as GoogleAuthSession from 'expo-auth-session/providers/google';
6import Constants, { ExecutionEnvironment } from 'expo-constants';
7import { maybeCompleteAuthSession } from 'expo-web-browser';
8import React from 'react';
9import { Platform, ScrollView, View } from 'react-native';
10
11import { getGUID } from '../../api/guid';
12import TitledPicker from '../../components/TitledPicker';
13import TitledSwitch from '../../components/TitledSwitch';
14import { AuthSection } from './AuthResult';
15import LegacyAuthSession from './LegacyAuthSession';
16
17maybeCompleteAuthSession();
18
19const isInClient = Constants.executionEnvironment === ExecutionEnvironment.StoreClient;
20
21const languages = [
22  { key: 'en', value: 'English' },
23  { key: 'pl', value: 'Polish' },
24  { key: 'nl', value: 'Dutch' },
25  { key: 'fi', value: 'Finnish' },
26];
27export default function AuthSessionScreen() {
28  const [useProxy, setProxy] = React.useState<boolean>(false);
29  const [usePKCE, setPKCE] = React.useState<boolean>(true);
30  const [prompt, setSwitch] = React.useState<undefined | AuthSession.Prompt>(undefined);
31  const [language, setLanguage] = React.useState<any>(languages[0].key);
32
33  return (
34    <View style={{ flex: 1, alignItems: 'center' }}>
35      <ScrollView
36        contentContainerStyle={{
37          paddingHorizontal: 12,
38          ...Platform.select({
39            default: {
40              maxWidth: '100%',
41            },
42            web: {
43              maxWidth: 640,
44            },
45          }),
46        }}>
47        <View style={{ marginBottom: 8 }}>
48          <H2>Settings</H2>
49          <TitledSwitch
50            title="Use Proxy"
51            disabled={Platform.OS === 'web'}
52            value={useProxy}
53            setValue={setProxy}
54          />
55          <TitledSwitch
56            title="Switch Accounts"
57            value={!!prompt}
58            setValue={value => setSwitch(value ? AuthSession.Prompt.SelectAccount : undefined)}
59          />
60          <TitledSwitch title="Use PKCE" value={usePKCE} setValue={setPKCE} />
61          <TitledPicker
62            items={languages}
63            title="Language"
64            value={language}
65            setValue={setLanguage}
66          />
67          <H4>ID: {Constants.manifest?.currentFullName || Constants.manifest?.id || 'unset'}</H4>
68        </View>
69        <H2>Services</H2>
70        <AuthSessionProviders
71          prompt={prompt}
72          usePKCE={usePKCE}
73          useProxy={useProxy}
74          language={language}
75        />
76        <H2>Legacy</H2>
77        <LegacyAuthSession />
78      </ScrollView>
79    </View>
80  );
81}
82
83AuthSessionScreen.navigationOptions = {
84  title: 'AuthSession',
85};
86
87function AuthSessionProviders(props: {
88  useProxy: boolean;
89  usePKCE: boolean;
90  prompt?: AuthSession.Prompt;
91  language: string;
92}) {
93  const { useProxy, usePKCE, prompt, language } = props;
94
95  const redirectUri = AuthSession.makeRedirectUri({
96    path: 'redirect',
97    preferLocalhost: Platform.select({ android: false, default: true }),
98    useProxy,
99  });
100
101  const options = {
102    useProxy,
103    usePKCE,
104    prompt,
105    redirectUri,
106    language,
107  };
108
109  const providers = [
110    Google,
111    GoogleFirebase,
112    Facebook,
113    Imgur,
114    Spotify,
115    Strava,
116    Twitch,
117    Dropbox,
118    Reddit,
119    Github,
120    Coinbase,
121    Uber,
122    Slack,
123    FitBit,
124    Okta,
125    Identity,
126    // Azure,
127  ];
128  return (
129    <View style={{ flex: 1 }}>
130      {providers.map((Provider, index) => (
131        <Provider key={`-${index}`} {...options} />
132      ))}
133    </View>
134  );
135}
136
137function Google({ prompt, language, usePKCE }: any) {
138  const [request, result, promptAsync] = GoogleAuthSession.useAuthRequest(
139    {
140      language,
141      expoClientId: '629683148649-qevd4mfvh06q14i4nl453r62sgd1p85d.apps.googleusercontent.com',
142      clientId: `${getGUID()}.apps.googleusercontent.com`,
143      selectAccount: !!prompt,
144      usePKCE,
145    },
146    {
147      path: 'redirect',
148      preferLocalhost: true,
149    }
150  );
151
152  React.useEffect(() => {
153    if (request && result?.type === 'success') {
154      console.log('Result: ', result.authentication);
155    }
156  }, [result]);
157
158  return (
159    <AuthSection
160      request={request}
161      title="google"
162      result={result}
163      promptAsync={() => promptAsync()}
164    />
165  );
166}
167
168function GoogleFirebase({ prompt, language, usePKCE }: any) {
169  const [request, result, promptAsync] = GoogleAuthSession.useIdTokenAuthRequest(
170    {
171      language,
172      expoClientId: '629683148649-qevd4mfvh06q14i4nl453r62sgd1p85d.apps.googleusercontent.com',
173      clientId: `${getGUID()}.apps.googleusercontent.com`,
174      selectAccount: !!prompt,
175      usePKCE,
176    },
177    {
178      path: 'redirect',
179      preferLocalhost: true,
180    }
181  );
182
183  React.useEffect(() => {
184    if (request && result?.type === 'success') {
185      console.log('Result:', result.params.id_token);
186    }
187  }, [result]);
188
189  return (
190    <AuthSection
191      request={request}
192      title="google_firebase"
193      result={result}
194      promptAsync={() => promptAsync()}
195    />
196  );
197}
198
199// Couldn't get this working. API is really confusing.
200// function Azure({ useProxy, prompt, usePKCE }: any) {
201//   const redirectUri = AuthSession.makeRedirectUri({
202//     path: 'redirect',
203//     preferLocalhost: true,
204//     useProxy,
205//     native: Platform.select<string>({
206//       ios: 'msauth.dev.expo.Payments://auth',
207//       android: 'msauth://dev.expo.payments/sZs4aocytGUGvP1%2BgFAavaPMPN0%3D',
208//     }),
209//   });
210
211//   // 'https://login.microsoftonline.com/your-tenant-id/v2.0',
212//   const discovery = AuthSession.useAutoDiscovery(
213//     'https://login.microsoftonline.com/f8cdef31-a31e-4b4a-93e4-5f571e91255a/v2.0'
214//   );
215//   const [request, result, promptAsync] = useAuthRequest(
216//     // config
217//     {
218//       clientId: '96891596-721b-4ae1-8e67-674809373165',
219//       redirectUri,
220//       prompt,
221//       extraParams: {
222//         domain_hint: 'live.com',
223//       },
224//       // redirectUri: 'msauth.{bundleId}://auth',
225//       scopes: ['openid', 'profile', 'email', 'offline_access'],
226//       usePKCE,
227//     },
228//     // discovery
229//     discovery
230//   );
231
232//   return (
233//     <AuthSection
234//       title="azure"
235//       disabled={isInClient}
236//       request={request}
237//       result={result}
238//       promptAsync={promptAsync}
239//       useProxy={useProxy}
240//     />
241//   );
242// }
243
244function Okta({ redirectUri, usePKCE, useProxy }: any) {
245  const discovery = AuthSession.useAutoDiscovery('https://dev-720924.okta.com/oauth2/default');
246  const [request, result, promptAsync] = useAuthRequest(
247    {
248      clientId: '0oa4su9fhp4F2F4Eg4x6',
249      redirectUri,
250      scopes: ['openid', 'profile'],
251      usePKCE,
252    },
253    discovery
254  );
255
256  return (
257    <AuthSection
258      title="okta"
259      request={request}
260      result={result}
261      promptAsync={promptAsync}
262      useProxy={useProxy}
263    />
264  );
265}
266
267// Reddit only allows one redirect uri per client Id
268// We'll only support bare, and proxy in this example
269// If the redirect is invalid with http instead of https on web, then the provider
270// will let you authenticate but it will redirect with no data and the page will appear broken.
271function Reddit({ redirectUri, prompt, usePKCE, useProxy }: any) {
272  let clientId: string;
273
274  if (isInClient) {
275    if (useProxy) {
276      // Using the proxy in the client.
277      // This expects the URI to be 'https://auth.expo.io/@community/native-component-list'
278      // so you'll need to be signed into community or be using the public demo
279      clientId = 'IlgcZIpcXF1eKw';
280    } else {
281      // // Normalize the host to `localhost` for other testers
282      clientId = 'CPc_adCUQGt9TA';
283    }
284  } else {
285    if (Platform.OS === 'web') {
286      // web apps with uri scheme `https://localhost:19006`
287      clientId = '9k_oYNO97ly-5w';
288    } else {
289      // Native bare apps with uri scheme `bareexpo`
290      clientId = '2OFsAA7h63LQJQ';
291    }
292  }
293
294  const [request, result, promptAsync] = useAuthRequest(
295    {
296      clientId,
297      clientSecret: '',
298      redirectUri,
299      prompt,
300      scopes: ['identity'],
301      usePKCE,
302    },
303    {
304      authorizationEndpoint: 'https://www.reddit.com/api/v1/authorize.compact',
305      tokenEndpoint: 'https://www.reddit.com/api/v1/access_token',
306    }
307  );
308
309  return (
310    <AuthSection
311      title="reddit"
312      request={request}
313      result={result}
314      promptAsync={promptAsync}
315      useProxy={useProxy}
316    />
317  );
318}
319
320// Imgur Docs https://api.imgur.com/oauth2
321// Create app https://api.imgur.com/oauth2/addclient
322function Imgur({ redirectUri, prompt, usePKCE, useProxy }: any) {
323  let clientId: string;
324
325  if (isInClient) {
326    if (useProxy) {
327      // Using the proxy in the client.
328      // This expects the URI to be 'https://auth.expo.io/@community/native-component-list'
329      // so you'll need to be signed into community or be using the public demo
330      clientId = '5287e6c03ffac8b';
331    } else {
332      // Normalize the host to `localhost` for other testers
333      // Expects: exp://127.0.0.1:19000/--/redirect
334      clientId = '7ab2f3cc75427a0';
335    }
336  } else {
337    if (Platform.OS === 'web') {
338      // web apps with uri scheme `https://localhost:19006`
339      clientId = '181b22d17a3743e';
340    } else {
341      // Native bare apps with uri scheme `bareexpo`
342      clientId = 'd839d91135a16cc';
343    }
344  }
345
346  const [request, result, promptAsync] = useAuthRequest(
347    {
348      clientId,
349      responseType: AuthSession.ResponseType.Token,
350      redirectUri,
351      scopes: [],
352      usePKCE,
353      prompt,
354    },
355    // discovery
356    {
357      authorizationEndpoint: 'https://api.imgur.com/oauth2/authorize',
358      tokenEndpoint: 'https://api.imgur.com/oauth2/token',
359    }
360  );
361
362  return (
363    <AuthSection
364      title="imgur"
365      request={request}
366      result={result}
367      promptAsync={() => promptAsync({ useProxy, windowFeatures: { width: 500, height: 750 } })}
368      useProxy={useProxy}
369    />
370  );
371}
372
373// TODO: Add button to test using an invalid redirect URI. This is a good example of AuthError.
374// Works for all platforms
375function Github({ redirectUri, prompt, usePKCE, useProxy }: any) {
376  let clientId: string;
377
378  if (isInClient) {
379    if (useProxy) {
380      // Using the proxy in the client.
381      clientId = '2e4298cafc7bc93ceab8';
382    } else {
383      clientId = '7eb5d82d8f160a434564';
384    }
385  } else {
386    if (Platform.OS === 'web') {
387      // web apps
388      clientId = 'fd9b07204f9d325e8f0e';
389    } else {
390      // Native bare apps with uri scheme `bareexpo`
391      clientId = '498f1fae3ae16f066f34';
392    }
393  }
394
395  const [request, result, promptAsync] = useAuthRequest(
396    {
397      clientId,
398      redirectUri,
399      scopes: ['identity'],
400      usePKCE,
401      prompt,
402    },
403    // discovery
404    {
405      authorizationEndpoint: 'https://github.com/login/oauth/authorize',
406      tokenEndpoint: 'https://github.com/login/oauth/access_token',
407      revocationEndpoint:
408        'https://github.com/settings/connections/applications/d529db5d7d81c2d50adf',
409    }
410  );
411
412  return (
413    <AuthSection
414      title="github"
415      request={request}
416      result={result}
417      promptAsync={() => promptAsync({ useProxy, windowFeatures: { width: 500, height: 750 } })}
418      useProxy={useProxy}
419    />
420  );
421}
422
423// I couldn't get access to any scopes
424// This never returns to the app after authenticating
425function Uber({ redirectUri, prompt, usePKCE, useProxy }: any) {
426  // https://developer.uber.com/docs/riders/guides/authentication/introduction
427  const [request, result, promptAsync] = useAuthRequest(
428    {
429      clientId: 'kTpT4xf8afVxifoWjx5Nhn-IFamZKp2x',
430      redirectUri,
431      scopes: [],
432      usePKCE,
433      prompt,
434      // Enable to test invalid_scope error
435      // scopes: ['invalid'],
436    },
437    // discovery
438    {
439      authorizationEndpoint: 'https://login.uber.com/oauth/v2/authorize',
440      tokenEndpoint: 'https://login.uber.com/oauth/v2/token',
441      revocationEndpoint: 'https://login.uber.com/oauth/v2/revoke',
442    }
443  );
444
445  return (
446    <AuthSection
447      title="uber"
448      request={request}
449      result={result}
450      promptAsync={promptAsync}
451      useProxy={useProxy}
452    />
453  );
454}
455
456// https://dev.fitbit.com/apps/new
457// Easy to setup
458// Only allows one redirect URI per app (clientId)
459// Refresh doesn't seem to return a new access token :[
460function FitBit({ redirectUri, prompt, usePKCE, useProxy }: any) {
461  let clientId: string;
462
463  if (isInClient) {
464    if (useProxy) {
465      // Using the proxy in the client.
466      clientId = '22BNXR';
467    } else {
468      // Client without proxy
469      clientId = '22BNXX';
470    }
471  } else {
472    if (Platform.OS === 'web') {
473      // web apps with uri scheme `https://localhost:19006`
474      clientId = '22BNXQ';
475    } else {
476      // Native bare apps with uri scheme `bareexpo`
477      clientId = '22BGYS';
478    }
479  }
480
481  const [request, result, promptAsync] = useAuthRequest(
482    {
483      clientId,
484      redirectUri,
485      scopes: ['activity', 'sleep'],
486      prompt,
487      usePKCE,
488    },
489    // discovery
490    {
491      authorizationEndpoint: 'https://www.fitbit.com/oauth2/authorize',
492      tokenEndpoint: 'https://api.fitbit.com/oauth2/token',
493      revocationEndpoint: 'https://api.fitbit.com/oauth2/revoke',
494    }
495  );
496
497  return (
498    <AuthSection
499      title="fitbit"
500      request={request}
501      result={result}
502      promptAsync={promptAsync}
503      useProxy={useProxy}
504    />
505  );
506}
507
508function Facebook({ usePKCE, useProxy, language }: any) {
509  const [request, result, promptAsync] = FacebookAuthSession.useAuthRequest(
510    {
511      clientId: '145668956753819',
512      usePKCE,
513      language,
514      scopes: ['user_likes'],
515    },
516    {
517      path: 'redirect',
518      preferLocalhost: true,
519      useProxy,
520    }
521  );
522  // Add fetch user example
523
524  return (
525    <AuthSection
526      title="facebook"
527      request={request}
528      result={result}
529      promptAsync={() => promptAsync()}
530    />
531  );
532}
533
534function Slack({ redirectUri, prompt, usePKCE, useProxy }: any) {
535  // https://api.slack.com/apps
536  // After you created an app, navigate to [Features > OAuth & Permissions]
537  // - Add a redirect URI Under [Redirect URLs]
538  // - Under [Scopes] add the scopes you want to request from the user
539  // Next go to [App Credentials] to get your client ID and client secret
540  // No refresh token or expiration is returned, assume the token lasts forever.
541  const [request, result, promptAsync] = useAuthRequest(
542    // config
543    {
544      clientId: '58692702102.1023025401076',
545      redirectUri,
546      scopes: ['emoji:read'],
547      prompt,
548      usePKCE,
549    },
550    // discovery
551    {
552      authorizationEndpoint: 'https://slack.com/oauth/authorize',
553      tokenEndpoint: 'https://slack.com/api/oauth.access',
554    }
555  );
556
557  return (
558    <AuthSection
559      title="slack"
560      request={request}
561      result={result}
562      promptAsync={promptAsync}
563      useProxy={useProxy}
564    />
565  );
566}
567
568// Works on all platforms
569function Spotify({ redirectUri, prompt, usePKCE, useProxy }: any) {
570  const [request, result, promptAsync] = useAuthRequest(
571    {
572      clientId: 'a946eadd241244fd88d0a4f3d7dea22f',
573      redirectUri,
574      scopes: ['user-read-email', 'playlist-modify-public', 'user-read-private'],
575      usePKCE,
576      extraParams: {
577        show_dialog: 'false',
578      },
579      prompt,
580    },
581    // discovery
582    {
583      authorizationEndpoint: 'https://accounts.spotify.com/authorize',
584      tokenEndpoint: 'https://accounts.spotify.com/api/token',
585    }
586  );
587
588  return (
589    <AuthSection
590      title="spotify"
591      request={request}
592      result={result}
593      promptAsync={promptAsync}
594      useProxy={useProxy}
595    />
596  );
597}
598
599function Strava({ redirectUri, prompt, usePKCE, useProxy }: any) {
600  const discovery = {
601    authorizationEndpoint: 'https://www.strava.com/oauth/mobile/authorize',
602    tokenEndpoint: 'https://www.strava.com/oauth/token',
603  };
604  const [request, result, promptAsync] = useAuthRequest(
605    {
606      clientId: '51935',
607      redirectUri,
608      scopes: ['activity:read_all'],
609      usePKCE,
610      prompt,
611    },
612    discovery
613  );
614
615  React.useEffect(() => {
616    if (request && result?.type === 'success' && result.params.code) {
617      AuthSession.exchangeCodeAsync(
618        {
619          clientId: request?.clientId,
620          redirectUri,
621          code: result.params.code,
622          extraParams: {
623            // You must use the extraParams variation of clientSecret.
624            client_secret: '...',
625          },
626        },
627        discovery
628      ).then(result => {
629        console.log('RES: ', result);
630      });
631    }
632  }, [result]);
633
634  return (
635    <AuthSection
636      title="strava"
637      request={request}
638      result={result}
639      promptAsync={promptAsync}
640      useProxy={useProxy}
641    />
642  );
643}
644
645// Works on all platforms
646function Identity({ redirectUri, prompt, useProxy }: any) {
647  const discovery = AuthSession.useAutoDiscovery('https://demo.identityserver.io');
648
649  const [request, result, promptAsync] = useAuthRequest(
650    {
651      clientId: 'native.code',
652      redirectUri,
653      prompt,
654      scopes: ['openid', 'profile', 'email', 'offline_access'],
655    },
656    discovery
657  );
658
659  return (
660    <AuthSection
661      title="identity4"
662      request={request}
663      result={result}
664      promptAsync={promptAsync}
665      useProxy={useProxy}
666    />
667  );
668}
669
670// Doesn't work with proxy
671function Coinbase({ redirectUri, prompt, usePKCE, useProxy }: any) {
672  const [request, result, promptAsync] = useAuthRequest(
673    {
674      clientId: '13b2bc8d9114b1cb6d0132cf60c162bc9c2d5ec29c2599003556edf81cc5db4e',
675      redirectUri,
676      prompt,
677      usePKCE,
678      scopes: ['wallet:accounts:read'],
679    },
680    // discovery
681    {
682      authorizationEndpoint: 'https://www.coinbase.com/oauth/authorize',
683      tokenEndpoint: 'https://api.coinbase.com/oauth/token',
684      revocationEndpoint: 'https://api.coinbase.com/oauth/revoke',
685    }
686  );
687
688  return (
689    <AuthSection
690      disabled={useProxy}
691      title="coinbase"
692      request={request}
693      result={result}
694      promptAsync={promptAsync}
695      useProxy={useProxy}
696    />
697  );
698}
699
700function Dropbox({ redirectUri, prompt, usePKCE, useProxy }: any) {
701  const [request, result, promptAsync] = useAuthRequest(
702    {
703      clientId: 'pjvyj0c5kxxrsfs',
704      redirectUri,
705      prompt,
706      usePKCE,
707      scopes: [],
708      responseType: AuthSession.ResponseType.Token,
709    },
710    // discovery
711    {
712      authorizationEndpoint: 'https://www.dropbox.com/oauth2/authorize',
713      tokenEndpoint: 'https://www.dropbox.com/oauth2/token',
714    }
715  );
716
717  return (
718    <AuthSection
719      disabled={usePKCE}
720      title="dropbox"
721      request={request}
722      result={result}
723      promptAsync={promptAsync}
724      useProxy={useProxy}
725    />
726  );
727}
728
729function Twitch({ redirectUri, prompt, usePKCE, useProxy }: any) {
730  const [request, result, promptAsync] = useAuthRequest(
731    {
732      clientId: 'r7jomrc4hiz5wm1wgdzmwr1ccb454h',
733      redirectUri,
734      prompt,
735      scopes: ['openid', 'user_read', 'analytics:read:games'],
736      usePKCE,
737    },
738    {
739      authorizationEndpoint: 'https://id.twitch.tv/oauth2/authorize',
740      tokenEndpoint: 'https://id.twitch.tv/oauth2/token',
741      revocationEndpoint: 'https://id.twitch.tv/oauth2/revoke',
742    }
743  );
744
745  return (
746    <AuthSection
747      disabled={useProxy}
748      title="twitch"
749      request={request}
750      result={result}
751      promptAsync={promptAsync}
752      useProxy={useProxy}
753    />
754  );
755}
756