From a69e1c1d709ce585cbfc239deff0a1a17111d528 Mon Sep 17 00:00:00 2001 From: Henning Perl Date: Thu, 27 Apr 2023 14:51:46 +0200 Subject: [PATCH] docs: describe OIDC for native apps (#1379) * docs: describe OIDC for native apps * Apply suggestions from code review Co-authored-by: Vincent --------- Co-authored-by: Vincent --- .vscode/tasks.json | 12 ++ docs/kratos/social-signin/96_native-apps.mdx | 147 +++++++++++++++++++ package-lock.json | 21 +++ package.json | 1 + src/sidebar.js | 1 + src/theme/CodeFromRemote.module.css | 12 ++ src/theme/CodeFromRemote.tsx | 27 +++- 7 files changed, 215 insertions(+), 6 deletions(-) create mode 100644 .vscode/tasks.json create mode 100644 docs/kratos/social-signin/96_native-apps.mdx diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 000000000..d181280c0 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,12 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "type": "npm", + "script": "start", + "label": "npm: start", + "detail": "docusaurus start", + "problemMatcher": [] + } + ] +} diff --git a/docs/kratos/social-signin/96_native-apps.mdx b/docs/kratos/social-signin/96_native-apps.mdx new file mode 100644 index 000000000..21a42ea1a --- /dev/null +++ b/docs/kratos/social-signin/96_native-apps.mdx @@ -0,0 +1,147 @@ +--- +id: native-apps +title: Social sign-in for native and mobile apps +sidebar_label: Native apps +--- + +```mdx-code-block +import Mermaid from '@theme/Mermaid'; +import CodeFromRemote from "@theme/CodeFromRemote" +``` + +# Social sign-in for native applications + +## Overview + +This page covers how to implement social sign-in for native applications via OIDC and OAuth 2.0. The user interaction looks like +this: + +- The user is presented with a login or registration screen that includes a social sign-in button. +- The user clicks the social sign-in button. A browser window opens (using + [ASWebAuthenticationSession](https://developer.apple.com/documentation/authenticationservices/aswebauthenticationsession) on iOS + or [Custom Tabs](https://developer.chrome.com/docs/android/custom-tabs/) on Android). +- The user authenticates with the identity provider and grants the application access to profile information. +- The user is redirected back to the application and is logged in. + +## The native app authentication flow + +From a high level, the native app initializes a login or registration flow and receives the first part of the session token +exchange code from the Ory Network. After the user performed the social sign-in, the user is redirected back to the native +application via an [iOS Universal Link](https://developer.apple.com/ios/universal-links/) or +[Android App Link](https://developer.android.com/training/app-links). The native application then exchanges the session token +exchange code for a session token. + +The flow looks like this: + +```mdx-code-block +>+A: Open login screen + A->>+O: create API flow with return_session_token_exchange_code=true + O->>A: flow.session_token_exchange_code: ... + U->>A: choose SSO provider + A->>O: submit form with provider=... + O->>A: Status 422 with err.response.data.redirect_browser_to= + A->>+B: go to + B->>+I: open + U->>I: login, consent + I->>-B: redirect to + B->>O: open + deactivate B + O->>A: redirect with App link to /?code=... + A->>O: exchange code for session token + O->>-A: session token +`}/> +``` + +### Implementation + +The following sections describe how to implement the native app authentication flow. The code examples are written in TypeScript +for React Native. The steps refer to the steps in the flow diagram above. + +#### Steps 1-5: Create an API flow with return_session_token_exchange_code=true + +The user opens the login screen (1) and the native application initializes a login or registration flow through the Ory Network +APIs (2) with the following parameters: + +- The flow is of type `api`. +- The `return_to` parameter is set to the URL of the native application. This URL is used to redirect the user back to the app + after the social sign-in. +- The `return_session_token_exchange_code` parameter is set to `true` to receive the session token exchange code in the response + (3). + + + +Upon rendering the form, the user selects the specific social sign-in provider (4). The native application submits the form to the +Ory Network (5). + +#### Steps 6-12: Open the identity provider authentication URL in a browser + +Ory Network returns a `422` status code if the user needs to perform a social sign-in from a flow of type `api` (6). The response +contains the URL of the identity provider in the `redirect_browser_to` field. + + + +[WebBrowser.openAuthSessionAsync](https://docs.expo.dev/versions/latest/sdk/webbrowser/#webbrowseropenauthsessionasyncurl-redirecturl-options) +opens a browser tab for authentication for the specified `url` (7+8). Implementations in other languages and frameworks may vary. + + + +The user authenticates with the identity provider and grants the application access to profile information (9). The identity +provider then issues a redirect to the Ory Network callback URL (10), which the browser follows (11). + +#### Steps 12-14: Exchange the session token exchange code for a session token + +Finally, Ory Network issues a session for the user and redirects the browser to the application's `return_to` URL (12). The native +application receives the second part of the session token exchange code in the `code` URL query parameter. In the example below, +the call to +[WebBrowser.maybeCompleteAuthSession](https://docs.expo.dev/versions/latest/sdk/webbrowser/#webbrowsermaybecompleteauthsessionoptions) +is used to close the browser tab and return the control flow to the code that awaited the call to +`WebBrowser.openAuthSessionAsync`. + + + +The native application then exchanges the session token exchange code for a session token (13) using the first part of the code +returned from the flow initialization, and the second part of the code returned from the `return_to` query parameter. + + + +Finally, the native application stores the session token in the secure storage (14) and redirects the user to the home screen. + + diff --git a/package-lock.json b/package-lock.json index 7b3bd6078..7c3d0d20d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -54,6 +54,7 @@ "@swc/core": "^1.2.139", "@swc/jest": "^0.2.17", "@types/jest": "^27.4.0", + "@types/node-fetch": "^2.6.3", "axios-retry": "^3.2.4", "fast-xml-parser": "^4.0.2", "jasmine-fail-fast": "^2.0.1", @@ -5260,6 +5261,16 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.18.tgz", "integrity": "sha512-eKj4f/BsN/qcculZiRSujogjvp5O/k4lOW5m35NopjZM/QwLOR075a8pJW5hD+Rtdm2DaCVPENS6KtSQnUD6BA==" }, + "node_modules/@types/node-fetch": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.3.tgz", + "integrity": "sha512-ETTL1mOEdq/sxUtgtOhKjyB2Irra4cjxksvcMUR5Zr4n+PxVhsCD9WS46oPbHL3et9Zde7CNRr+WUNlcHvsX+w==", + "dev": true, + "dependencies": { + "@types/node": "*", + "form-data": "^3.0.0" + } + }, "node_modules/@types/parse-json": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", @@ -25522,6 +25533,16 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.18.tgz", "integrity": "sha512-eKj4f/BsN/qcculZiRSujogjvp5O/k4lOW5m35NopjZM/QwLOR075a8pJW5hD+Rtdm2DaCVPENS6KtSQnUD6BA==" }, + "@types/node-fetch": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.3.tgz", + "integrity": "sha512-ETTL1mOEdq/sxUtgtOhKjyB2Irra4cjxksvcMUR5Zr4n+PxVhsCD9WS46oPbHL3et9Zde7CNRr+WUNlcHvsX+w==", + "dev": true, + "requires": { + "@types/node": "*", + "form-data": "^3.0.0" + } + }, "@types/parse-json": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", diff --git a/package.json b/package.json index 871ae46cd..732225e23 100644 --- a/package.json +++ b/package.json @@ -74,6 +74,7 @@ "@swc/core": "^1.2.139", "@swc/jest": "^0.2.17", "@types/jest": "^27.4.0", + "@types/node-fetch": "^2.6.3", "axios-retry": "^3.2.4", "fast-xml-parser": "^4.0.2", "jasmine-fail-fast": "^2.0.1", diff --git a/src/sidebar.js b/src/sidebar.js index 35db5fa8f..da706d224 100644 --- a/src/sidebar.js +++ b/src/sidebar.js @@ -88,6 +88,7 @@ module.exports = { "kratos/social-signin/get-tokens", "kratos/social-signin/data-mapping", "kratos/social-signin/account-linking", + "kratos/social-signin/native-apps", ], }, "identities/sign-in/check-session", diff --git a/src/theme/CodeFromRemote.module.css b/src/theme/CodeFromRemote.module.css index c3c038838..e066cc48c 100644 --- a/src/theme/CodeFromRemote.module.css +++ b/src/theme/CodeFromRemote.module.css @@ -1,3 +1,15 @@ .container { margin-bottom: var(--ifm-leading); } + +.codeblock { + margin-bottom: 0.4em; +} + +.link { + font-size: 0.8em; + color: #06b6d4; + text-align: end; + margin-bottom: var(--ifm-leading); + text-decoration: underline; +} diff --git a/src/theme/CodeFromRemote.tsx b/src/theme/CodeFromRemote.tsx index 403da072d..ca151fbe9 100644 --- a/src/theme/CodeFromRemote.tsx +++ b/src/theme/CodeFromRemote.tsx @@ -58,20 +58,24 @@ const findLine = (needle: string | undefined, haystack: Array) => { const transform = ({ startAt, endAt }: { startAt?: string; endAt?: string }) => - (content: string) => { + (content: string): [string, number, number] => { let lines = content.split("\n") + let startLineNum = 1 + let endLineNum = lines.length const startIndex = findLine(startAt, lines) if (startIndex > 0) { lines = ["// ...", ...lines.slice(startIndex, -1)] + startLineNum = startIndex + 1 } const endIndex = findLine(endAt, lines) if (endIndex > 0) { lines = [...lines.slice(0, endIndex + 1), "// ..."] + endLineNum = startLineNum + endIndex - 1 } - return lines.join("\n") + return [lines.join("\n"), startLineNum, endLineNum] } const CodeFromRemote = (props: { @@ -80,9 +84,12 @@ const CodeFromRemote = (props: { contentOverride?: string startAt?: string endAt?: string + showLink?: boolean }) => { const { src, title, contentOverride } = props const [content, setContent] = useState(contentOverride || "") + const [startLineNum, setStartLineNum] = useState(0) + const [endLineNum, setEndLineNum] = useState(0) useEffect(() => { if (contentOverride) { @@ -96,21 +103,29 @@ const CodeFromRemote = (props: { ) .then((body) => body.text()) .then(transform(props)) - .then(setContent) + .then(([content, startLineNum, endLineNum]) => { + setContent(content) + setStartLineNum(startLineNum) + setEndLineNum(endLineNum) + }) .catch(console.error) }, [contentOverride, src]) const lang = `language-${detectLanguage(src)}` - const metaString = `title="${title || findPath(src)}"` return ( ) }