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