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