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