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