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