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