UNPKG

react-native-azure-ad-test

Version:

A Azure AD authentication library implementation uses pure react native API (no native library needed).

269 lines (227 loc) 9.45 kB
// @flow import React, {Component} from 'react' import {WebView, Dimensions, AsyncStorage, Platform} from 'react-native' import CONST from './const.js' import ReactNativeAD from './ReactNativeAD.js' import Timer from 'react-timer-mixin' import log from './logger' const loginUrl = 'https://login.microsoftonline.com/<tenant id>/oauth2/v2.0/authorize' const tokenUrl = 'https://login.microsoftonline.com/common/oauth2/v2.0/token' export default class ADLoginView extends React.Component { props : { onSuccess? : ?Function, renderError? : ?Function, needLogout? : bool, style : any, onURLChange : Function, context : ReactNativeAD, hideAfterLogin? : bool, userAgent?: string }; state : { page : string, visible : bool }; static defaultProps = { authority_host : loginUrl, tenant : 'common', onSuccess : () => {}, renderError: () => {}, onPageRequest : null }; _needRedirect : bool; _onTokenGranted : ()=>{}; _lock : bool; _accessToken : Object; constructor(props:any){ super(props) if(!this.props.context instanceof ReactNativeAD) throw new Error('property `context` of ADLoginView should be an instance of ReactNativeAD, but got', this.props.context) let context = this.props.context let tenant = context.getConfig().tenant this._needRedirect = this.props.needLogout || false this.state = { page : this._getLoginUrl(tenant || 'common'), visible : true, } this._lock = false } componentWillUpdate(nextProps:any, nextState:any):any { if(this.state.visible === nextState.visible && this.state.page === nextState.page) return false return true } componentDidUpdate(prevProps:any, prevState:any) { if(prevState.visible !== this.state.visible) this.props.onVisibilityChange && this.props.onVisibilityChange(this.state.visible) log.debug('ADLoginView updated.') } componentWillReceiveProps(nextProps){ if(!this.props.needLogout && nextProps.needLogout) { let context = this.props.context let tenant = context.getConfig().tenant this._needRedirect = nextProps.needLogout || false this.setState({ page : this._getLoginUrl(tenant || 'common'), visible : true }) } } render() { // Fix visibility problem on Android webview let js = `document.getElementsByTagName('body')[0].style.height = '${Dimensions.get('window').height}px';` let renderError = this.props.renderError || function () { } return ( this.state.visible ? (<WebView ref="ADLoginView" automaticallyAdjustContentInsets={false} style={[this.props.style, { flex:1, alignSelf : 'stretch', width : Dimensions.get('window').width, height : Dimensions.get('window').height }]} source={{uri: this.state.page}} javaScriptEnabled={true} domStorageEnabled={true} onLoadEnd={()=>{ if(this._needRedirect){ this._needRedirect = false let tenant = this.props.context.getConfig().tenant || 'common' this.setState({page : this._getLoginUrl(tenant)}) } }} decelerationRate="normal" javaScriptEnabledAndroid={true} onNavigationStateChange={this._handleADToken.bind(this)} onShouldStartLoadWithRequest={(e) => { return true }} userAgent={this.props.userAgent} renderError={() => renderError(this.refs.ADLoginView.reload)} startInLoadingState={false} injectedJavaScript={js} scalesPageToFit={true}/>) : null ) } /** * Get authority host URI, * @param {string} tenant Custom tenant ID, this filed is optional, default * values is `common`. * @return {string} The Authority host URI. */ _getLoginUrl(tenant:string='common'):string{ let authUrl = String(this.props.authority_host || loginUrl).replace('<tenant id>', tenant) let context = this.props.context || null let redirect = context.getConfig().redirect_uri let prompt = context.getConfig().prompt let login_hint = context.getConfig().login_hint let scope = context.getConfig().scope if(context !== null) { let result = `${authUrl}?response_type=code` + `&client_id=${context.getConfig().client_id}` + (redirect ? `&redirect_url=${context.getConfig().redirect_uri}&nonce=rnad-${Date.now()}` : '') + (prompt ? `&prompt=${context.getConfig().prompt}` : '') + (scope ? `&scope=${context.getConfig().scope}` : '') + (login_hint ? `&login_hint=${context.getConfig().login_hint}` : '') console.log(result); if(this._needRedirect) result = `https://login.windows.net/${this.props.context.getConfig().client_id}/oauth2/logout` return result } else { throw new Error('AD context should not be null/undefined.') } } /** * An interceptor for handling webview url change, when it detects possible * authorization code in url, it will triggers authentication flow. * @param {object} e Navigation state change event object. */ _handleADToken(e:{ url:string }):any{ log.verbose('ADLoginView navigate to', e.url) if(this._lock) return true let code = /((\?|\&)code\=)[^\&]+/.exec(e.url) if(this._needRedirect) { // this._needRedirect = false return true } if(this.props.onURLChange) this.props.onURLChange(e) if( code !== null ){ this._lock = true log.verbose('ADLoginView._handleADToken code=', code[0]) code = String(code[0]).replace(/(\?|\&)?code\=/,'') this.setState({visible : !this.props.hideAfterLogin}) this.props.onVisibilityChange && this.props.onVisibilityChange(false) this._getResourceAccessToken(code).catch((err) => { log.error('ADLoginView._getResourceAccessToken', err) }) return true } return true } /** * Check required properies and show error. * @param {ReactNativeADConfig} config Configration object input. */ _checkProperties(config:any) { ['client_id', 'redirect_uri', 'authority_host'].forEach((prop)=>{ if( !config.hasOwnProperty(prop) ) throw new Error(`ReactNativeAD config object must have \`${prop}\` property`) }) } /** * Get access token for each resoureces * @param {string} code The authorization code from `onNavigationStateChange` * callback. * @return {Promise<void>} */ _getResourceAccessToken(code:string):Promise { let context = this.props.context if(!context) throw new Error('property `context` of ADLoginView should not be null/undefined') let adConfig:ADConfig = this.props.context.getConfig() let {client_id=null, redirect_uri=null, client_secret=null, resources=null} = adConfig // Transform resource string to array if( typeof resources === 'string') resources = [resources] else if(!Array.isArray(resources) || resources.length===0) resources = ["common"]; log.verbose('ADLoginView get access token for resources=', resources) /* NOTE: as of 11/15/2018, MS has changed the token behavior in that we cannot re-use the same auth-code more than once to get all the resource tokens. * Instead we must use the refresh token result from the first token request to get other resource tokens. * https://docs.microsoft.com/en-us/azure/active-directory/fundamentals/whats-new#change-notice-authorization-codes-will-no-longer-be-available-for-reuse * https://social.msdn.microsoft.com/Forums/en-US/4192e141-309a-4dd6-a5c9-f1a8ce32f4ca/aadsts54005-oauth2-authorization-code-was-already-redeemed?forum=WindowsAzureAD */ let promises:Array<Promise> = [] let [resourceFirst, ...resourcesOther] = resources; let config = { client_id, redirect_uri, code, client_secret } return context.grantAccessToken(CONST.GRANT_TYPE.AUTHORIZATION_CODE, config).then(cred => { //remove AUTHORIZATION_CODE specific properties because we are now going to use the REFRESH_TOKEN for all the remaining resources delete config.code; delete config.redirect_uri; config.refresh_token = cred.response.refresh_token; //get array of promises for all the resource token acquisitions to perform in parallel let promises = resourcesOther.map(resource => { let cfg = Object.assign({},config,{resource}); return context.grantAccessToken(CONST.GRANT_TYPE.REFRESH_TOKEN, cfg); }); //wait on the resource promises, then finalize return Promise.all(promises).then((resps:Array<GrantTokenResp>) => { log.verbose('ADLoginView response access info ', resps) if(!this.props.context) throw new Error('value of property `context` is invalid=', this.props.context) let context = this.props.context let onSuccess = this.props.onSuccess || function(){} // trigger loggined finished event if(context !== null && typeof this.props.onSuccess === 'function') onSuccess(context.getCredentials()) this._lock = false }).catch((err) => { throw new Error('Failed to acquire token for resources', err.stack) }); }); } }