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