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