Sign in with DiiL

Use an authorization code when you need the user's phone or email, or want to call DiiL later without the user.

const code = await Diil.requestAuthCode()       // DiiL shows the consent sheet the first time
await fetch('/login', { method: 'POST', body: JSON.stringify({ code }) })

The code is valid for 60 seconds and works once. Exchange it on your server.

The address of the DiiL API (the base for the endpoints below) is shown in For business → My apps in the DiiL app.

Exchange the code: POST /oauth/token #

Send application/x-www-form-urlencoded (JSON is accepted too). Client credentials go in the body or as HTTP Basic (Authorization: Basic base64(client_id:client_secret)).

grant_type=authorization_code&code=Xk9…&client_id=21&client_secret=diil_sk_…
{ "access_token": "eyJhbGciOiJIUzI1NiJ9…", "token_type": "Bearer", "expires_in": 3600,
  "refresh_token": "diil_rt_…", "refresh_expires_in": 2592000, "scope": "profile username email" }
  • access_token lives 1 hour. Use it for the userinfo endpoint. Treat it as opaque.
  • refresh_token lives 30 days and is single use. Every refresh returns a new one: store it and drop the old one. Presenting an already used refresh token revokes the whole chain (we assume it leaked), and the user has to sign in again. Presenting a used code does the same to the tokens issued from it.

Refresh #

grant_type=refresh_token&refresh_token=diil_rt_…&client_id=21&client_secret=diil_sk_…

The new tokens never carry more scopes than the user currently grants.

Read the profile: GET /oauth/userinfo #

Authorization: Bearer <access_token>

{ "sub": "4", "id": 4, "firstName": "Anna", "lastName": null, "languageCode": "en",
  "username": "anna", "avatarUrl": "https://…",
  "phone": "+66900000004", "email": "anna@example.com", "emailVerified": true,
  "scope": "profile username avatar phone email" }

Only the granted scopes' fields are present. If the user revokes access, the secret is rotated or the token expires, you get 401 invalid_token.

Sign out: POST /oauth/revoke #

token=<access or refresh token>&client_id=…&client_secret=… returns 200 (always, as in RFC 7009). Either token revokes every refresh token of that consent, so no new access tokens can be issued. A revoked access token also stops working at once; other access tokens already issued expire within the hour. Call it when the user signs out of your app.

Errors #

OAuth-style JSON: {"error": "invalid_grant", "error_description": "The code has expired"}.

Status error Meaning
400 invalid_request A parameter is missing or the body is malformed.
400 invalid_grant Unknown, expired or already used code or refresh token, or the user revoked access.
400 unsupported_grant_type Not authorization_code or refresh_token.
401 invalid_client Unknown client_id or wrong client_secret.
401 invalid_token Userinfo: bad, expired or revoked access token (also WWW-Authenticate).
403 invalid_origin A browser request from an origin not in allowedOrigins.
429 slow_down Rate limit; see Retry-After.

Rate limits #

  • /oauth/token and /oauth/revoke: 60 requests per minute per client.
  • 10 failed client authentications per 15 minutes per IP.
  • /oauth/userinfo: 120 per minute per user and app.
  • requestAuthCode: 30 per minute per user.

CORS #

Browser calls to /oauth/* must come from allowedOrigins. Keep the code exchange on your server anyway: the client secret must never reach the browser.