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