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