Sign in with Monotree
Partner apps can sign Monotree users in with standard OpenID Connect: the authorization code flow with PKCE. A user can also open your app from the Monotree app and arrive signed in, with no login prompt. This page is for the partner developer.
Overview
Monotree is an OpenID Connect provider. Your app or site is a relying party. A user reaches you in one of two ways, and both end in the same authorization code flow, so you build it once.
| The user starts | What happens |
|---|---|
| In the Monotree app, by tapping a link to your app | Monotree opens your login initiation URI with a one-time login_hint. You start the normal flow and pass the hint along. The user is signed in with no prompt. See Flow 1. |
| In your app or on your site, by tapping "Sign in with Monotree" | You start the normal flow. The user signs in to Monotree. See Flow 2. |
Every Monotree customer runs on its own server and is its own issuer, for example https://acme.monotree.com. There is no shared Monotree login host. You already know which customer a user belongs to, so you know which issuer to use.
Only the authorization code flow is supported, and Monotree issues no refresh tokens. Keep your own session after sign-in. For data about the user beyond who they are, use the Open API.
Discovery and endpoints
Fetch the discovery document per customer and read the endpoints from it. Do not hard-code them.
GET https://acme.monotree.com/.well-known/openid-configuration| Endpoint | Path on the issuer | Purpose |
|---|---|---|
| Authorization | /oauth/authorize | Start of every sign-in |
| Token | /oauth/token | Exchange the code for tokens |
| UserInfo | /oauth/userinfo | The same claims as the id_token, with the access token |
| JWKS | /oauth/jwks | Public keys to verify id_token signatures |
The discovery document also lists:
scopes_supported:openid,profile,emailresponse_types_supported:codecode_challenge_methods_supported:S256id_token_signing_alg_values_supported:RS256token_endpoint_auth_methods_supported:none(native apps),client_secret_basicandclient_secret_post(web backends). We recommendclient_secret_basic.
For an example, see the demo server's document: demo.monotree.com/.well-known/openid-configuration.
Discovery, JWKS and UserInfo answer 404 on a Monotree server until a partner client is registered there.
Registering a client
Monotree registers clients per Monotree customer. There is no dynamic registration. Send us the following for each customer you connect:
| You send | Example | Notes |
|---|---|---|
| Client type | public | public for a native app (PKCE, no secret). confidential for a web backend (client secret) |
| Name | Your app | Your app's name in Monotree's records |
| Redirect URIs | https://app.partner.example/auth/monotree/callback | Exact match, no wildcards. Native apps can use a universal link / App Link, or a private-use scheme such as com.partner.app:/auth/monotree. Plain http only on localhost |
| Login initiation URI | https://app.partner.example/auth/monotree/initiate | Where launches from the Monotree app start (Flow 1) |
| Target link prefix | https://app.partner.example/ | Every target_link_uri must start with it. Must end with / |
You get back a client_id, and a client_secret for a confidential client. Each customer is its own issuer, so you get a different client_id on each Monotree server.
Flow 1: launch from the Monotree app
This is OpenID Connect third-party initiated login, the same pattern as LTI 1.3. The user is already signed in to the Monotree app. Monotree gives you a one-time login_hint that signs them in to exactly one authorization request.
- The user taps a link to your app in the Monotree app.
- Monotree opens your login initiation URI with
iss,login_hintandtarget_link_uri. - You start a normal authorization request to the issuer, with the
login_hintunchanged. - Monotree redirects straight back to your
redirect_uriwith acode. No screen is shown. - You exchange the code for tokens, sign the user in, and open
target_link_uri.
Monotree opens your login initiation URI (GET):
https://app.partner.example/auth/monotree/initiate
?iss=https%3A%2F%2Facme.monotree.com
&login_hint=lt_9f3c2a7e5d1b4c08a6e2
&target_link_uri=https%3A%2F%2Fapp.partner.example%2Fitems%2F42
&client_id=7iss: which Monotree customer. Reject issuers you are not registered with.login_hint: opaque, one-time, valid for 60 seconds. Pass it on unchanged. Never store or log it.target_link_uri: where to take the user after sign-in. Check that it starts with your target link prefix.
You start a normal authorization request, adding login_hint:
GET https://acme.monotree.com/oauth/authorize
?response_type=code
&client_id=7
&redirect_uri=https%3A%2F%2Fapp.partner.example%2Fauth%2Fmonotree%2Fcallback
&scope=openid%20profile%20email
&state=af0ifjsldkj
&nonce=n-0S6_WzA2Mj
&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
&code_challenge_method=S256
&login_hint=lt_9f3c2a7e5d1b4c08a6e2- Never send
prompt=loginon a launch. It asks the user to sign in again, which defeats the launch. - A native app opens this in the system browser sheet (
ASWebAuthenticationSessionon iOS, Custom Tabs on Android). Use an ephemeral session for launches (prefersEphemeralWebBrowserSessionon iOS). - A launch signs the user in to this one request only. It does not leave a Monotree session in the browser.
- If the hint has expired or was already used, the user sees the Monotree login page instead, as in Flow 2, and can still sign in.
Links in the Monotree app: a CMS menu link of type External app launches through Flow 1 automatically when its URL starts with your target link prefix.
Flow 2: sign in started in your app
The user taps "Sign in with Monotree" in your app or on your site. It is the same authorization request as Flow 1, without login_hint.
- You send the user to the issuer's authorization endpoint.
- The user sees the Monotree login page and signs in, with their Monotree password or through their organisation's identity provider if one is set up. If they already have a Monotree session in that browser, they skip this step.
- Monotree redirects back to your
redirect_uriwith acode. - You exchange the code for tokens.
Some customers require two-factor login. There, an employee with no phone number on their Monotree profile cannot complete a sign-in started in your app. Launches from the Monotree app (Flow 1) always work, and so does signing in through the organisation's identity provider.
There is no consent screen: the customer approved your app when Monotree registered it.
prompt and max_age are supported. With prompt=login, or when the user's sign-in is older than max_age seconds, the user signs in again.
Exchanging the code
Token request from a native app (public client):
POST https://acme.monotree.com/oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&code=SplxlOBeZQQYbYS6WxSbIA
&redirect_uri=https%3A%2F%2Fapp.partner.example%2Fauth%2Fmonotree%2Fcallback
&client_id=7
&code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXkA confidential client authenticates with HTTP Basic (client_id:client_secret) instead of sending client_id in the body. Codes are single-use and expire after 60 seconds. If a used code is sent again, the access token it gave is revoked.
The token endpoint does not allow cross-origin browser calls. Call it from your backend or from the native app, not from JavaScript in a web page.
Response:
{
"token_type": "Bearer",
"access_token": "eyJ0eXAiOiJKV1Qi...",
"expires_in": 600,
"id_token": "eyJraWQiOiIyMDI2LTA5IiwiYWxnIjoiUlMyNTYifQ..."
}PKCE
- Public clients (native apps): PKCE with
S256is required. - Confidential clients: PKCE with
S256is recommended. plainis never accepted.
Tokens, scopes and claims
The id_token says who the user is, and it is small on purpose. For more (department, groups, manager), call the Open API with a token the customer issues you, using sub as the user ID.
| Claim | Scope | Value |
|---|---|---|
iss | openid | The customer's issuer, for example https://acme.monotree.com |
sub | openid | The Monotree user ID as a string, for example "512". The same ID the Open API uses |
aud | openid | Your client_id |
exp, iat | openid | Expiry and issue time |
nonce | openid | The nonce from your authorization request |
auth_time | openid | When the user signed in to Monotree, or the moment of the launch |
name, given_name, family_name | profile | The user's name as shown in Monotree |
picture | profile | Avatar URL, when the user has one |
email | The user's current email |
subis unique per issuer. Two customers can have a user with the samesub, so key users oniss+sub.- Do not key users on
email: it can change. There is noemail_verifiedclaim.
Example id_token payload:
{
"iss": "https://acme.monotree.com",
"sub": "512",
"aud": "7",
"exp": 1790255100,
"iat": 1790254800,
"auth_time": 1790254795,
"nonce": "n-0S6_WzA2Mj",
"name": "Jane Doe",
"given_name": "Jane",
"family_name": "Doe",
"email": "jane@example.com"
}| Token | Lifetime |
|---|---|
Launch login_hint | 60 seconds, one use |
| Authorization code | 60 seconds, one use |
| id_token | 5 minutes |
| Access token (for UserInfo only) | 10 minutes |
| Refresh token | Not issued |
The access token is only for /oauth/userinfo. It does not work on the Open API.
Signing keys
id_tokens are signed with RS256. Each Monotree server has its own key pair, published in its JWKS with a kid.
- Pick the key by the
kidin the token header. Never pin a single key. - Cache the JWKS, and on an unknown
kidfetch it again once before you reject the token. - Keys rotate yearly, and at once after a suspected compromise. A new key appears in the JWKS 7 days before Monotree starts signing with it, and the old key stays 7 days after.
The JWKS URL is the only source of keys. Monotree never sends keys by email.
What Monotree does not support
- Refresh tokens and offline access.
- Request objects: an authorization request with
requestorrequest_uriis refused. - Dynamic client registration for partners.
- Single logout. Signing out of Monotree does not sign the user out of your app.
- Groups, department or role claims in the id_token. Use the Open API.
Partner checklist
Check every id_token:
- The signature against the issuer's JWKS, by
kid. AlgorithmRS256only. issis exactly the issuer you started the flow with.audcontains yourclient_id.expis in the future (allow at most 60 seconds of clock skew).nonceis the one you sent.
In every flow:
- PKCE with
S256, with a newcode_verifierfor each attempt. - A new
state, checked when the user comes back. - Identify the user by
iss+sub, never by email. - Pass
login_hinton unchanged. Do not store or log it. - Only follow a
target_link_urithat starts with your target link prefix. - Never send
prompt=loginon a launch.
Native apps (RFC 8252):
- Open the authorization request in the system browser sheet, never in an embedded WebView. Use an ephemeral session for launches.
- Use a universal link / App Link for the login initiation URI, so a launch from Monotree opens your app.
- Register as a public client. Do not ship a client secret in the app.