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