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