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