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