Skip to content

Commit a69e1c1

Browse files
hperlvinckr
andauthored
docs: describe OIDC for native apps (#1379)
* docs: describe OIDC for native apps * Apply suggestions from code review Co-authored-by: Vincent <[email protected]> --------- Co-authored-by: Vincent <[email protected]>
1 parent 1915ea5 commit a69e1c1

File tree

7 files changed

+215
-6
lines changed

7 files changed

+215
-6
lines changed

.vscode/tasks.json

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"version": "2.0.0",
3+
"tasks": [
4+
{
5+
"type": "npm",
6+
"script": "start",
7+
"label": "npm: start",
8+
"detail": "docusaurus start",
9+
"problemMatcher": []
10+
}
11+
]
12+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
---
2+
id: native-apps
3+
title: Social sign-in for native and mobile apps
4+
sidebar_label: Native apps
5+
---
6+
7+
```mdx-code-block
8+
import Mermaid from '@theme/Mermaid';
9+
import CodeFromRemote from "@theme/CodeFromRemote"
10+
```
11+
12+
# Social sign-in for native applications
13+
14+
## Overview
15+
16+
This page covers how to implement social sign-in for native applications via OIDC and OAuth 2.0. The user interaction looks like
17+
this:
18+
19+
- The user is presented with a login or registration screen that includes a social sign-in button.
20+
- The user clicks the social sign-in button. A browser window opens (using
21+
[ASWebAuthenticationSession](https://developer.apple.com/documentation/authenticationservices/aswebauthenticationsession) on iOS
22+
or [Custom Tabs](https://developer.chrome.com/docs/android/custom-tabs/) on Android).
23+
- The user authenticates with the identity provider and grants the application access to profile information.
24+
- The user is redirected back to the application and is logged in.
25+
26+
## The native app authentication flow
27+
28+
From a high level, the native app initializes a login or registration flow and receives the first part of the session token
29+
exchange code from the Ory Network. After the user performed the social sign-in, the user is redirected back to the native
30+
application via an [iOS Universal Link](https://developer.apple.com/ios/universal-links/) or
31+
[Android App Link](https://developer.android.com/training/app-links). The native application then exchanges the session token
32+
exchange code for a session token.
33+
34+
The flow looks like this:
35+
36+
```mdx-code-block
37+
<Mermaid chart={`
38+
sequenceDiagram
39+
actor U as user
40+
participant A as Native App
41+
participant B as Browser
42+
participant O as Ory Network
43+
participant I as Identity Provider
44+
autonumber
45+
U->>+A: Open login screen
46+
A->>+O: create API flow with return_session_token_exchange_code=true
47+
O->>A: flow.session_token_exchange_code: ...
48+
U->>A: choose SSO provider
49+
A->>O: submit form with provider=...
50+
O->>A: Status 422 with err.response.data.redirect_browser_to=<IdP auth URL>
51+
A->>+B: go to <IdP auth URL>
52+
B->>+I: open <IdP auth URL>
53+
U->>I: login, consent
54+
I->>-B: redirect to <IdP callback URL>
55+
B->>O: open <IdP callback URL>
56+
deactivate B
57+
O->>A: redirect with App link to <return to>/?code=...
58+
A->>O: exchange code for session token
59+
O->>-A: session token
60+
`}/>
61+
```
62+
63+
### Implementation
64+
65+
The following sections describe how to implement the native app authentication flow. The code examples are written in TypeScript
66+
for React Native. The steps refer to the steps in the flow diagram above.
67+
68+
#### Steps 1-5: Create an API flow with return_session_token_exchange_code=true
69+
70+
The user opens the login screen (1) and the native application initializes a login or registration flow through the Ory Network
71+
APIs (2) with the following parameters:
72+
73+
- The flow is of type `api`.
74+
- 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
75+
after the social sign-in.
76+
- The `return_session_token_exchange_code` parameter is set to `true` to receive the session token exchange code in the response
77+
(3).
78+
79+
<CodeFromRemote
80+
lang="ts"
81+
src="https://github.com/ory/kratos-selfservice-ui-react-native/blob/2ef61d58aac508ad8890c01dac4b5fd4740265ff/src/components/Routes/Login.tsx"
82+
startAt=" const initializeFlow = () =>"
83+
endAt=" .then(({ data: f }) => setFlow(f))"
84+
/>
85+
86+
Upon rendering the form, the user selects the specific social sign-in provider (4). The native application submits the form to the
87+
Ory Network (5).
88+
89+
#### Steps 6-12: Open the identity provider authentication URL in a browser
90+
91+
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
92+
contains the URL of the identity provider in the `redirect_browser_to` field.
93+
94+
<CodeFromRemote
95+
lang="ts"
96+
src="https://github.com/ory/kratos-selfservice-ui-react-native/blob/2ef61d58aac508ad8890c01dac4b5fd4740265ff/src/helpers/form.tsx"
97+
startAt=" case 422:"
98+
endAt=" )"
99+
/>
100+
101+
[WebBrowser.openAuthSessionAsync](https://docs.expo.dev/versions/latest/sdk/webbrowser/#webbrowseropenauthsessionasyncurl-redirecturl-options)
102+
opens a browser tab for authentication for the specified `url` (7+8). Implementations in other languages and frameworks may vary.
103+
104+
<CodeFromRemote
105+
lang="ts"
106+
src="https://github.com/ory/kratos-selfservice-ui-react-native/blob/2ef61d58aac508ad8890c01dac4b5fd4740265ff/src/helpers/form.tsx"
107+
startAt=" const result = await WebBrowser.openAuthSessionAsync("
108+
endAt=" )"
109+
/>
110+
111+
The user authenticates with the identity provider and grants the application access to profile information (9). The identity
112+
provider then issues a redirect to the Ory Network callback URL (10), which the browser follows (11).
113+
114+
#### Steps 12-14: Exchange the session token exchange code for a session token
115+
116+
Finally, Ory Network issues a session for the user and redirects the browser to the application's `return_to` URL (12). The native
117+
application receives the second part of the session token exchange code in the `code` URL query parameter. In the example below,
118+
the call to
119+
[WebBrowser.maybeCompleteAuthSession](https://docs.expo.dev/versions/latest/sdk/webbrowser/#webbrowsermaybecompleteauthsessionoptions)
120+
is used to close the browser tab and return the control flow to the code that awaited the call to
121+
`WebBrowser.openAuthSessionAsync`.
122+
123+
<CodeFromRemote
124+
lang="tsx"
125+
src="https://github.com/ory/kratos-selfservice-ui-react-native/blob/2ef61d58aac508ad8890c01dac4b5fd4740265ff/src/components/Routes/Callback.tsx"
126+
startAt="const Callback = (props: Props) => {"
127+
endAt=" )"
128+
/>
129+
130+
The native application then exchanges the session token exchange code for a session token (13) using the first part of the code
131+
returned from the flow initialization, and the second part of the code returned from the `return_to` query parameter.
132+
133+
<CodeFromRemote
134+
lang="ts"
135+
src="https://github.com/ory/kratos-selfservice-ui-react-native/blob/2ef61d58aac508ad8890c01dac4b5fd4740265ff/src/helpers/form.tsx"
136+
startAt=" const fetchToken = (params: FrontendApiExchangeSessionTokenRequest) =>"
137+
endAt=' newOrySdk("").exchangeSessionToken(params)'
138+
/>
139+
140+
Finally, the native application stores the session token in the secure storage (14) and redirects the user to the home screen.
141+
142+
<CodeFromRemote
143+
lang="ts"
144+
src="https://github.com/ory/kratos-selfservice-ui-react-native/blob/2ef61d58aac508ad8890c01dac4b5fd4740265ff/src/helpers/form.tsx"
145+
startAt=" const setToken = (res: Awaited<ReturnType<typeof fetchToken>>) =>"
146+
endAt=" })"
147+
/>

package-lock.json

+21
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@
7474
"@swc/core": "^1.2.139",
7575
"@swc/jest": "^0.2.17",
7676
"@types/jest": "^27.4.0",
77+
"@types/node-fetch": "^2.6.3",
7778
"axios-retry": "^3.2.4",
7879
"fast-xml-parser": "^4.0.2",
7980
"jasmine-fail-fast": "^2.0.1",

src/sidebar.js

+1
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ module.exports = {
8888
"kratos/social-signin/get-tokens",
8989
"kratos/social-signin/data-mapping",
9090
"kratos/social-signin/account-linking",
91+
"kratos/social-signin/native-apps",
9192
],
9293
},
9394
"identities/sign-in/check-session",

src/theme/CodeFromRemote.module.css

+12
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,15 @@
11
.container {
22
margin-bottom: var(--ifm-leading);
33
}
4+
5+
.codeblock {
6+
margin-bottom: 0.4em;
7+
}
8+
9+
.link {
10+
font-size: 0.8em;
11+
color: #06b6d4;
12+
text-align: end;
13+
margin-bottom: var(--ifm-leading);
14+
text-decoration: underline;
15+
}

src/theme/CodeFromRemote.tsx

+21-6
Original file line numberDiff line numberDiff line change
@@ -58,20 +58,24 @@ const findLine = (needle: string | undefined, haystack: Array<string>) => {
5858

5959
const transform =
6060
({ startAt, endAt }: { startAt?: string; endAt?: string }) =>
61-
(content: string) => {
61+
(content: string): [string, number, number] => {
6262
let lines = content.split("\n")
63+
let startLineNum = 1
64+
let endLineNum = lines.length
6365

6466
const startIndex = findLine(startAt, lines)
6567
if (startIndex > 0) {
6668
lines = ["// ...", ...lines.slice(startIndex, -1)]
69+
startLineNum = startIndex + 1
6770
}
6871

6972
const endIndex = findLine(endAt, lines)
7073
if (endIndex > 0) {
7174
lines = [...lines.slice(0, endIndex + 1), "// ..."]
75+
endLineNum = startLineNum + endIndex - 1
7276
}
7377

74-
return lines.join("\n")
78+
return [lines.join("\n"), startLineNum, endLineNum]
7579
}
7680

7781
const CodeFromRemote = (props: {
@@ -80,9 +84,12 @@ const CodeFromRemote = (props: {
8084
contentOverride?: string
8185
startAt?: string
8286
endAt?: string
87+
showLink?: boolean
8388
}) => {
8489
const { src, title, contentOverride } = props
8590
const [content, setContent] = useState(contentOverride || "")
91+
const [startLineNum, setStartLineNum] = useState(0)
92+
const [endLineNum, setEndLineNum] = useState(0)
8693

8794
useEffect(() => {
8895
if (contentOverride) {
@@ -96,21 +103,29 @@ const CodeFromRemote = (props: {
96103
)
97104
.then((body) => body.text())
98105
.then(transform(props))
99-
.then(setContent)
106+
.then(([content, startLineNum, endLineNum]) => {
107+
setContent(content)
108+
setStartLineNum(startLineNum)
109+
setEndLineNum(endLineNum)
110+
})
100111
.catch(console.error)
101112
}, [contentOverride, src])
102113

103114
const lang = `language-${detectLanguage(src)}`
104-
const metaString = `title="${title || findPath(src)}"`
105115

106116
return (
107117
<div className={styles.container}>
108118
<CodeBlock
109-
metastring={metaString}
110-
className={lang}
119+
title={title || findPath(src)}
120+
className={`${lang} ${styles.codeblock}`}
111121
children={content}
112122
showLineNumbers={true}
113123
/>
124+
<div className={styles.link}>
125+
<a href={src + `#L${startLineNum}-L${endLineNum}`}>
126+
see full code on GitHub
127+
</a>
128+
</div>
114129
</div>
115130
)
116131
}

0 commit comments

Comments
 (0)