From 2bace6fc2c0f7b1bd6522265f27d5424b969f4b9 Mon Sep 17 00:00:00 2001 From: Roman Melnyk Date: Wed, 16 Dec 2020 12:31:03 +0200 Subject: [PATCH] #166 Implement apple web based authentication using react native WebView. --- README.md | 146 +++++++++++++++------ lib/AppleAuthWebView.js | 282 ++++++++++++++++++++++++++++++++++++++++ lib/index.js | 6 + 3 files changed, 391 insertions(+), 43 deletions(-) create mode 100644 lib/AppleAuthWebView.js diff --git a/README.md b/README.md index 160339a29..a1111f52f 100644 --- a/README.md +++ b/README.md @@ -221,52 +221,110 @@ async function onAppleButtonPress() { } ``` - -### Web (not react-native-web, but that may come as a follow-on, this is pure web at the moment) - +### WebView #### 1. Initial set-up -- Ensure you follow the android steps above. -- Install the [web counterpart](https://github.com/A-Tokyo/react-apple-signin-auth) `yarn add react-apple-signin-auth` in your web project. +- Make sure to correctly configure your Apple developer account to allow for proper web based authentication. +- Install the [React Native WebView](https://github.com/react-native-webview/react-native-webview) `yarn add react-native-webview` (or) `npm i react-native-webview` in your project. [Link native dependencies](https://github.com/react-native-webview/react-native-webview/blob/master/docs/Getting-Started.md#2-link-native-dependencies). +- Your backend needs to implement web based authentification -#### 2. Implement the login process on web +#### 2. Implement the login process ```js -import AppleSignin from 'react-apple-signin-auth'; - -/** Apple Signin button */ -const MyAppleSigninButton = ({ ...rest }) => ( - { - console.log(response); - // { - // "authorization": { - // "state": "[STATE]", - // "code": "[CODE]", - // "id_token": "[ID_TOKEN]" - // }, - // "user": { - // "email": "[EMAIL]", - // "name": { - // "firstName": "[FIRST_NAME]", - // "lastName": "[LAST_NAME]" - // } - // } - // } - }} - /> -); - -export default MyAppleSigninButton; + +// App.js +import React from 'react'; +import { + View, + TouchableWithoutFeedback + Text +} from 'react-native'; +import { + appleAuth, + appleAuthAndroid, + AppleAuthWebView // Internaly using WebView +} from "@invertase/react-native-apple-authentication"; + +function onAppleLoginWebViewButtonPress() { + + // https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_js/incorporating_sign_in_with_apple_into_other_platforms + const appleAuthConfig = { + // The Service ID you registered with Apple + clientId: "com.example.client-web", + + // Return URL added to your Apple dev console. It must still match the URL you provided to Apple. + redirectUri: "https://example.com/auth/callback", + + // The type of response requested - code, id_token, or both. + responseType: "code id_token", + + // The amount of user information requested from Apple. + scope: "name email" + + // Random nonce value that will be SHA256 hashed before sending to Apple. + // nonce: nonce, + + // Unique state value used to prevent CSRF attacks. A UUID will be generated if nothing is provided. + // state: state + }; + + this.setState( + { + appleAuthConfig: appleAuthConfig + } + ); +} + +function onAppleAuthResponse(responseContent) { + + // Handle your server response (after login - apple redirects to your server url) + console.log("onAppleAuthResponse responseContent", responseContent); +} + +// Apple authentication requires API 19+, so we check before showing the login button +// If no iOS or Android is supported than we use webView fallback with custom button +function App() { + + render() { + const appleAuthConfig = this.state.appleAuthConfig; + + if (appleAuthConfig) { + return ( + { + // return ( + // + // ); + // } + // } + onResponse={this.onAppleAuthResponse} + /> + ); + } + } + + return ( + + + { + // (appleAuth.isSupported || appleAuthAndroid.isSupported) ? ( + // + // ) // else add webView view button + } + + + + Sign in with Apple + + + + ); +} ``` #### 3. Verify serverside @@ -322,6 +380,8 @@ export default MyAppleSigninButton; - [AndroidResponseType](docs/enums/_lib_index_d_.androidresponsetype.md) - [AndroidScope](docs/enums/_lib_index_d_.androidscope.md) +### WebView Config +- https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_js/incorporating_sign_in_with_apple_into_other_platforms ## FAQs diff --git a/lib/AppleAuthWebView.js b/lib/AppleAuthWebView.js new file mode 100644 index 000000000..48d91e79f --- /dev/null +++ b/lib/AppleAuthWebView.js @@ -0,0 +1,282 @@ +import React from "react"; +import { + View, + ActivityIndicator +} from "react-native"; +import { + WebView +} from "react-native-webview"; + +export default class AppleAuthWebView extends React.PureComponent { + constructor(props) { + super(props); + + const me = this; + const onResponse = props.onResponse; + const config = props.config; // || (props.route && props.route.params && props.route.params.config); + const loadingIndicator = props.loadingIndicator; + const clientId = config.clientId; + const redirectUri = config.redirectUri; + const scope = config.scope; + const responseType = config.responseType; + const state = config.state; + const rawNonce = config.rawNonce; + const nonce = config.nonce; + + // Input data validating + // https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_js/incorporating_sign_in_with_apple_into_other_platforms#3332113 + if (typeof onResponse !== "function") { + throw new Error( + "AppleAuthWebView.constructor 'onResponse' is required and must be a function." + ); + } + + if (loadingIndicator && typeof loadingIndicator !== "function") { + throw new Error( + "AppleAuthWebView.constructor 'loadingIndicator' required as a function, if provided." + ); + } + + if (typeof config !== "object") { + throw new Error( + "AppleAuthWebView.constructor 'config' is required and must be an object." + ); + } else { + if (typeof clientId !== "string") { + throw new Error( + "AppleAuthWebView.constructor 'clientId' is required and must be a string." + ); + } + + if (typeof redirectUri !== "string") { + throw new Error( + "AppleAuthWebView.constructor 'redirectUri' is required and must be a string." + ); + } + + if (typeof scope !== "string") { + throw new Error( + "AppleAuthWebView.constructor 'scope' is required and must be a string." + ); + } else if ( + scope !== "name" && + scope !== "email" && + scope !== "name email" + ) { + throw new Error( + "AppleAuthWebView.constructor 'scope' is invalid. Possible values 'name', 'email', 'name email'" + ); + } + + if (responseType && typeof responseType !== "string") { + throw new Error( + "AppleAuthWebView.constructor 'responseType' required as a string, if provided." + ); + } + + if ( + responseType && + responseType !== "code" && + responseType !== "id_token" && + responseType !== "code id_token" + ) { + throw new Error( + "AppleAuthWebView.constructor 'responseType' is invalid. Possible values 'code', 'id_token', 'code id_token'" + ); + } + + if (state && typeof state !== "string") { + throw new Error( + "AppleAuthWebView.constructor 'state' required as a string, if provided." + ); + } + + if (rawNonce && typeof rawNonce !== "string") { + throw new Error( + "AppleAuthWebView.constructor 'rawNonce' required as a string, if provided." + ); + } + + if (nonce && typeof nonce !== "string") { + throw new Error( + "AppleAuthWebView.constructor 'nonce' required as a string, if provided." + ); + } + } + + me.state = { + config: config, + isAppleInitialWebPageLoading: false, + isRedirectResultContentLoading: false + }; + + me.appleDirectAuthorizationUrl = "https://appleid.apple.com/auth/authorize"; + } + + onNavigationStateChange = (navigationState) => { + const me = this; + const url = navigationState.url; + const loading = navigationState.loading; + const appleAuthUrl = me.appleDirectAuthorizationUrl; + + if ( + url.substr(0, appleAuthUrl.length) === appleAuthUrl && + loading !== me.state.isAppleInitialWebPageLoading + ) { + me.setState( + { + isAppleInitialWebPageLoading: loading + } + ); + + return; + } + + // Loading redirected page + const redirectUri = me.state.config.redirectUri; + + if ( + url.substr(0, redirectUri.length) === redirectUri && + !me.state.isAppleInitialWebPageLoading + ) { + me.setState( + { + isRedirectResultContentLoading: true // To hide our server response + } + ); + + me.webview.injectJavaScript( + ` + function checkContent() { + if (document && document.documentElement) { + window.ReactNativeWebView.postMessage( + JSON.stringify( + { + href: window.location.href, + pageContent: document.documentElement.innerText + } + ) + ); + } else { + setTimeout( + function () { + checkContent(); + }, + 300 + ); + } + } + + checkContent(); + true; + ` + ); + } + } + + onMessageFromRedirectedPage = (event) => { + const me = this; + const message = event.nativeEvent.data; + + try { + const data = JSON.parse(message); + const redirectUri = me.state.config.redirectUri; + + if (data.href.substr(0, redirectUri.length) === redirectUri) { + me.props.onResponse(data.pageContent); + } + } catch (error) { + console.error("AppleAuthWebView.onMessageFromRedirectedPage Cannot parse web page message.", message); + } + } + + /** + * Build an Apple URL that supports `form_post` + * Incorporating Sign in with Apple into Other Platforms + * https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_js/incorporating_sign_in_with_apple_into_other_platforms. + */ + getDirectAuthorizationSourceUri = (config) => { + const me = this; + const scope = config.scope; + const state = config.state; + const nonce = config.nonce; + + return { + uri: me.appleDirectAuthorizationUrl + + "?client_id=" + encodeURIComponent(config.clientId) + + "&redirect_uri=" + encodeURIComponent(config.redirectUri) + + "&response_type=" + encodeURIComponent(config.responseType) + + (scope ? "&scope=" + encodeURIComponent(scope) : "") + + "&response_mode=form_post" + + (state ? "&state=" + encodeURIComponent(state) : "") + + (nonce ? "&nonce=" + encodeURIComponent(nonce) : "") + }; + } + + onComponentRefenceReady = (ref) => { + const me = this; + + me.webview = ref; + // me.webview.stopLoading(); + // me.webview.injectJavaScript(redirectTo); + } + + render() { + const me = this; + const state = me.state; + const isLoading = state.isAppleInitialWebPageLoading || state.isRedirectResultContentLoading; + + return ( + + { + ( + + ) + } + + { + isLoading && ( + me.props.loadingIndicator ? ( + + { + me.props.loadingIndicator() + } + + ) : ( + + ) + ) + } + + ); + } +} diff --git a/lib/index.js b/lib/index.js index aae539353..5287ea64d 100644 --- a/lib/index.js +++ b/lib/index.js @@ -18,6 +18,7 @@ import version from './version'; import { NativeModules } from 'react-native'; import AppleAuthModule from './AppleAuthModule'; +import AppleAuthWebView from './AppleAuthWebView'; const { RNAppleAuthModule, RNAppleAuthModuleAndroid } = NativeModules; @@ -45,3 +46,8 @@ export const appleAuthAndroid = RNAppleAuthModuleAndroid ? { Scope: RNAppleAuthModuleAndroid.Scope, ResponseType: RNAppleAuthModuleAndroid.ResponseType, } : {}; + +/** + * WebView + */ +export { default as AppleAuthWebView } from './AppleAuthWebView';