diff --git a/docs/developer-guide/Authentication-And-Identity.asciidoc b/docs/developer-guide/Authentication-And-Identity.asciidoc index f1005644017..48bce537dc6 100644 --- a/docs/developer-guide/Authentication-And-Identity.asciidoc +++ b/docs/developer-guide/Authentication-And-Identity.asciidoc @@ -6,10 +6,14 @@ This chapter covers Codename One's modern sign-in stack: OpenID Connect, Sign in The stack is rebuilt around two new primitives: * `com.codename1.io.oidc.OidcClient` -- a full OpenID Connect / OAuth 2.0 client with PKCE, discovery, refresh and token persistence. -* `com.codename1.io.oidc.SystemBrowser` -- routes the sign-in step through the platform's hardened sign-in surface (`ASWebAuthenticationSession` on iOS, Android Custom Tabs, the user's default browser on JavaSE / Web). +* `com.codename1.io.oidc.SystemBrowser` -- routes the sign-in step through the platform's hardened sign-in surface where one exists: `ASWebAuthenticationSession` on iOS and Android Custom Tabs. Elsewhere it falls back to `BrowserWindow`, and what that means differs: the Web port opens a real top-level browser window, while JavaSE renders a JavaFX `WebView` inside the app. The desktop one is an embedded view, so a provider that blocks those can refuse a simulator sign-in that works on a device. The Web one has a constraint of its own -- it's a `window.open()` call, and it happens after the discovery request comes back rather than inside the click that started the sign-in, so a browser that has already expired the transient activation blocks it. Tell your users to allow popups for the site, or start the flow from an `OidcClient` you configured ahead of time instead of `discover()`. And point the redirect URI at a page on the app's own origin: the port watches the popup by reading its location, the same-origin policy blocks that read once the popup navigates elsewhere, and the fallback reports the authorization URL again -- so a callback hosted anywhere else never gets recognized and `authorize()` simply never completes. That applies to Apple's `form_post` bridge too, which has to live on that origin rather than beside the Services ID. Every provider-specific class is a thin layer on top of these primitives, so once you learn the underlying client you understand the whole stack. +image::img/oidc-provider-stack.svg[The OpenID Connect stack and the two paths that leave it,scaledwidth=95%] + +Two places step outside it, and both are deliberate. `AppleSignIn` calls the native Apple sheet whenever the platform offers one, which on iOS 13+ means neither primitive is involved; the OpenID Connect path is what it falls back to everywhere else. And `FirebaseAuth` isn't an OpenID Connect provider at all -- you reach it by handing it an ID token you already obtained, which `FacebookConnect` can't give you because Facebook's flow issues an access token and nothing else. + WARNING: The legacy `com.codename1.io.Oauth2` class is **deprecated**. It opens an in-app `WebBrowser`, which modern identity providers (Google, Apple, Microsoft, Facebook) now refuse to render -- they detect the embedded view and block the page. The new `OidcClient` works the same on every supported platform without that limitation. See <> for a migration recipe. === Why the change @@ -40,6 +44,14 @@ That call: . Exchanges the code for tokens on the discovered token endpoint. . Verifies `state` and `nonce`, decodes the ID token, and persists the tokens via the default `TokenStore`. +image::img/oidc-sign-in-flow.svg[The sign-in flow across the app the platform browser and the provider,scaledwidth=95%] + +The token exchange is the leg worth noticing: it goes straight from your app to the provider's token endpoint, carrying the PKCE verifier, and the browser has already done its job by then. A provider that treats your app as a public client wants no secret on that request, which is the arrangement PKCE exists to make safe. Apple is the exception on its non-native path and wants a `client_secret` JWT your server mints, which is what <> is about. + +Only the redirect URL crosses back from the browser, which is why Apple's non-iOS path needs a server in front of it: `AppleSignIn` asks for `response_mode=form_post`, so Apple POSTs `code` and `state` to your redirect URI rather than putting them in the query string, and `OidcClient` parses the URL alone. The page at that URI has to turn the POST into a redirect carrying the same parameters, or the sign-in fails with `STATE_MISMATCH`. + +Whether the token endpoint hands back a refresh token is up to the request: the quick start above asks for `openid`, `email` and `profile` and nothing more, so Google returns none and there is nothing for `refreshIfExpired` to renew. `GoogleConnect.signIn` adds `access_type=offline` and `prompt=consent` for exactly that reason. + ==== Picking a redirect URI For mobile apps, use a *custom scheme* unique to your app, for example `com.example.app:/oauth2redirect`. Register the scheme with the OS via the build hints below so the system browser can hand the redirect back to your app. @@ -104,6 +116,8 @@ NOTE: Apple only returns the user's name and email on the *first* authorization. Apple requires a separate *Services ID* (a "web client") for non-iOS environments and a `client_secret` JWT generated by your server. The recipe lives at <>. +On Android there is a second thing to arrange. The Services ID callback is an HTTPS URL, and the Android sign-in surface hands control back by matching the scheme of the redirect it's waiting for, so the `com.example.app` custom scheme registered earlier in this chapter does nothing for it. The callback host and path need their own verified App Link: an `android.xintent_filter` carrying an `android:autoVerify="true"` filter for `android:scheme="https"` on that host, and an `assetlinks.json` served from it. <> has both, and says how to put that filter and the custom-scheme one in a single hint value -- the hint holds one string, so an app doing both needs them together. + [[apple-services-id-setup]] ==== One-time Apple setup diff --git a/docs/developer-guide/img/oidc-provider-stack.svg b/docs/developer-guide/img/oidc-provider-stack.svg new file mode 100644 index 00000000000..10b1222e259 --- /dev/null +++ b/docs/developer-guide/img/oidc-provider-stack.svg @@ -0,0 +1,71 @@ + + + The OpenID Connect stack, and the two paths that leave it + + GoogleConnect + system browser, + no native SDK + + + MicrosoftConnect + Entra ID, + any tenant + + + FacebookConnect + OAuth 2.0 only -- + issues no ID token + + + Auth0Connect + a plain OIDC + provider + + + AppleSignIn + native on iOS 13+, + OIDC elsewhere + + + + + + FirebaseAuth + not OpenID Connect: Identity Toolkit REST. Federate it + with an ID token from Google, Microsoft, Apple or Auth0. + + + + The native Apple sheet + ASAuthorizationAppleIDProvider on iOS 13+. AppleSignIn + takes it whenever it can, and touches neither primitive. + + + + OidcClient + discovery, PKCE (S256), state verification, a nonce check when the ID + token carries one, code exchange, the refresh-token grant, and + persistence through TokenStore + + + + SystemBrowser + + iOS + ASWebAuthenticationSession + + + Android + Custom Tabs + + + JavaSE + an in-app WebView + an embedded view; providers may refuse it + + + Web + a browser popup window + a real window, not embedded + + diff --git a/docs/developer-guide/img/oidc-sign-in-flow.svg b/docs/developer-guide/img/oidc-sign-in-flow.svg new file mode 100644 index 00000000000..4224cb4bdc0 --- /dev/null +++ b/docs/developer-guide/img/oidc-sign-in-flow.svg @@ -0,0 +1,77 @@ + + + + Your app + + The platform's sign-in surface + + The identity provider + + OidcClient.authorize() + discovery document, + PKCE verifier + challenge, + state and nonce + + SystemBrowser + ASWebAuthenticationSession (iOS), + Custom Tabs (Android), + a BrowserWindow elsewhere + + Authorization endpoint + the user signs in here, + with a passkey if the + provider offers one + + Redirect + code + state sent to + your redirect URI, in the + query string + + Handed back to the app + the custom scheme or HTTPS URL + you registered with the OS. Only the + URL crosses back, never a POST body + + Verify state + parsed out of that URL; + a mismatch aborts before + any token call + + Token exchange + code + PKCE verifier; + a public client sends + no secret + + Token endpoint + id_token and access_token, + plus refresh_token only if + offline access was asked for + + TokenStore + tokens persisted; + refreshIfExpired renews them + when a refresh token came back + + + + + + + + + + + + + + + + + + + the authorization request, + carrying the PKCE challenge + code + PKCE verifier + tokens, straight to the app -- + the browser is not in this leg +