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