Use your own OAuth server
Your users may already have accounts at an identity server you run, such as Keycloak, Auth0 or Zitadel. A Frigear mobile or browser app can sign them in there, instead of at frigear.nu. This page lists what that issuer must support, and how to set it up for the common ones.
Auth in client mode signs users in as a public OAuth application, with PKCE (Proof Key for Code Exchange). It needs neither OAuth Server nor frigear.nu. Any issuer that meets the checklist below works:
- Point
authentication.client.issuerat it. - Register the app there as a public application.
The rest of the app is unchanged: the mobile shell, $api and the sign-in page. Why an
app in a browser or on a device has no secret is on
Public and confidential applications.
The issuer checklist
Check each item at the issuer before you debug the app.
- Discovery.
GET <issuer>/.well-known/openid-configurationanswers JSON whoseissuerequals the configuredissuerexactly, including any trailing path.oauth-authorization-server(RFC 8414) works too. - PKCE with S256. The issuer accepts
code_challenge_method=S256. When the discovery document hascode_challenge_methods_supported, it must listS256. The client refuses the issuer otherwise. - A public application with no secret. The application has
token_endpoint_auth_method: none. The token and revoke endpoints accept aclient_idwithout aclient_secret. - Exact redirect URIs. Register each URI exactly, with no wildcards. See Redirect URIs.
- CORS from the webview. These endpoints answer cross-origin requests (CORS) from
capacitor://localhost(iOS),https://localhost(Android) and your dev origin, with theAuthorizationheader allowed: the token, revocation, userinfo and discovery endpoints, and your API. Use exact origins, never*. The authorization endpoint is a navigation and needs no CORS. - Refresh tokens. The application is allowed the
refresh_tokengrant and theoffline_accessscope. The issuer rotates refresh tokens: a used one is replaced, and a replayed one revokes the family. -
issin the authorization response. The issuer returnsissin the redirect (RFC 9207) and advertisesauthorization_response_iss_parameter_supported: true. - An ID token. The scopes include
openid.
NUXT_PUBLIC_AUTHENTICATION_CLIENT_CLIENT_SECRET fails.Discovery. Without a discovery document, set authentication.client.discovery: false. The client then uses the
paths of OAuth Server, or the ones you list in authentication.client.endpoints. Listing endpoints alone does not
turn discovery off.
Refresh tokens. Add offline_access to authentication.client.scopes. Otherwise the session ends when the access
token expires.
iss. The client rejects a response whose iss differs from the issuer. It also rejects a response without iss
when the issuer advertises support for it.
ID token. The signed-in user (useUserSession().user) is mapped from the ID token's claims: sub, name,
email, picture and role. A sign-in whose response has no valid ID token fails. A userinfo endpoint is needed
only with client.userinfo: true, or when your client.mapUser calls userinfo().
Redirect URIs
| App | Redirect URI | Example |
|---|---|---|
| Web | <origin>/auth/callback | https://app.example.com/auth/callback, and http://localhost:3000/auth/callback for development |
| Native | <appId in lowercase>:/auth/callback | com.example.app:/auth/callback |
A native redirect URI is a custom scheme with one slash and no host. Some providers call it a private-use URI scheme,
or a "native" application type. When you change callbackPath or native.redirectUri, register the changed URIs.
OAuth Server
With OAuth Server on the issuer, the app is a public application: PKCE only, and no
secret. Define it in config, so its client_id is the same in every environment. The full walkthrough, including
confidential applications, is Create an OAuth application.
export default defineNuxtConfig({
oauthServer: {
// Your own API that the app calls with the access token.
cors: { paths: ['/api/'] },
clients: [{
id: 'bills',
name: 'Bills',
redirectUris: ['nu.frigear.bills:/auth/callback'],
// Accepted under `nuxt dev` only.
devRedirectUris: ['http://localhost:3001/auth/callback'],
// The webviews' origins; native redirect URIs have none to derive.
corsOrigins: ['capacitor://localhost', 'https://localhost'],
}],
},
})
| Option | Purpose |
|---|---|
oauthServer.clients | Public applications with a fixed client_id. NUXT_OAUTH_SERVER_CLIENTS replaces the list with JSON. |
oauthServer.cors.origins | Exact origins that may call the token, revoke, userinfo and discovery endpoints. The origins of configured clients are added. |
oauthServer.cors.paths | Path prefixes of your own API that those origins may call with a bearer token. |
The app side:
export default defineNuxtConfig({
modules: ['@nuxt/ui', '@frigear-nu/auth'],
authentication: {
mode: 'client',
client: {
issuer: 'https://id.example.com',
clientId: 'bills',
scopes: ['openid', 'profile', 'email', 'offline_access'],
native: { scheme: 'nu.frigear.bills' },
},
},
})
cors.paths need OAuth Server 0.4.0 or later. On an older
version, register the application in the admin area as a public application. Add the native redirect URI
to it, and list the webview origins in oauthServer.cors.origins. See OAuth Server.frigear.nu
frigear.nu runs OAuth Server. Use issuer: 'https://frigear.nu'.
- Registration. Staff of frigear.nu register the application by hand in the admin area, with the client type Public and the redirect URIs above. Ask one of them: there is no self-service registration.
- CORS. It already allows
capacitor://localhostandhttps://localhost, so the webview needs nothing more. - Your services. Services behind the app accept the same access token through OAuth Resource. So one sign-in serves every service.
Keycloak
Setting names vary between versions. The intent is what matters.
- Create a client in the realm: Client authentication off (public), Standard flow on and Direct access grants off.
- Set Valid redirect URIs to the web and native callback URIs.
- Set Web origins to
capacitor://localhost,https://localhostand your dev origin. This sets CORS. - Under Advanced, set Proof Key for Code Exchange Code Challenge Method to
S256. - Turn on Revoke Refresh Token in the realm's token settings. That turns on refresh token rotation.
- Make
offline_accessa client scope of the client.
The issuer is https://<host>/realms/<realm>.
Auth0
- Create an application of type Native for apps in a Capacitor webview, or Single Page Application for a browser only.
- Add the callback URIs to Allowed Callback URLs.
- Add the origins to Allowed Origins (CORS) and Allowed Web Origins.
- Enable Refresh Token Rotation in the application's settings.
- When you call an API registered at Auth0, add
audiencetoauthentication.client.authorizationParams, and allow offline access on that API.
The issuer is https://<tenant>.auth0.com/, or your custom domain.
Zitadel
- Create an application of type Native, with authentication method None and PKCE, or of type User Agent.
- Register the redirect URIs. Native apps accept custom-scheme redirects.
- Set Refresh Token under grant types.
- Add the origins under Additional origins.
- To register
http://localhostredirects, set the application to Development mode.
The issuer is your instance URL.
Troubleshooting
| Symptom | Likely cause |
|---|---|
The browser shows invalid_redirect_uri or redirect_uri_mismatch | The URI is not registered exactly. Compare scheme, case, slashes and the path (appId:/auth/callback, one slash). |
config error: issuer mismatch | The discovery document's issuer differs from authentication.client.issuer, for example http against https or another path. |
config error: invalid_client | The application is confidential, or its id is wrong. Register a public application. |
| Sign-in works in the browser, then the token request fails in the app | CORS: the token endpoint does not allow capacitor://localhost or https://localhost. |
| Signed in, but the session ends when the access token expires | No offline_access scope, no refresh_token grant, or the provider issues no refresh token to public applications. |
| Signed out after the first refresh | The provider rotated the refresh token and the response did not carry the new one, or two apps share one refresh token. |
The $api calls get 401 and the user is signed out | The API does not accept the token (audience, issuer) even after a refresh. |
The $api calls get 401, and the user stays signed in | No token was sent: the request's origin is not client.apiBase, the issuer or one of client.apiOrigins. |
| The sign-in fails right after the redirect back | No ID token in the token response. Add openid to the scopes, and check that the issuer returns one to this application. |
| The app does not come back after signing in on a device | The native URL scheme is not registered in the iOS or Android project. Run frigear-mobile native and frigear-mobile doctor. |
| A production build fails with a client secret error | NUXT_PUBLIC_AUTHENTICATION_CLIENT_CLIENT_SECRET is set. Remove it, and use a public application. |
Next steps
- Create an OAuth application – register the app at an issuer that runs OAuth Server.
- Mobile starter – run an app that signs in this way.
- OAuth Resource – accept the same access token in your other services.
HTTP routes
The URL, handler, error, response and test conventions every module API follows, and the server toolkit that implements them.
Create an OAuth application
Register an app at an issuer that runs OAuth Server, as a confidential application in the admin area or a public one in the admin area or nuxt.config.