Skip to content

JWKS & JWT verification

logi issues RS256 JWTs as access tokens. An RP verifies the signature statelessly, or calls /oauth/userinfo to check revocation.

JWKS endpoint

GET /.well-known/jwks.json

Example response:

json
{
  "keys": [
    {
      "kty": "RSA",
      "use": "sig",
      "alg": "RS256",
      "kid": "ffaa476406e8abec",
      "n": "xqJbzP...",
      "e": "AQAB"
    }
  ]
}

A Cache-Control: public, max-age=3600 header is attached, so it can be cached for one hour.

Key rotation

  • A new key is issued and the kid changes once per quarter
  • The old key is kept in the JWKS for a 90-day grace period (to verify previously issued tokens)
  • New issuance is signed only with the active kid

When implementing as an RP, refresh the JWKS cache on a 1-hour to 1-day basis. On a verification failure, force a refresh once and retry.

Payload schema

json
{
  "iss": "https://api.1pass.dev",
  "sub": "42",
  "aud": "logi_a1b2c3d4...",
  "exp": 1734567890,
  "iat": 1734566990,
  "jti": "9d6f...-...-...",
  "scope": "profile email"
}

Verification examples by language

ts
import { jwtVerify, createRemoteJWKSet } from "jose";

const jwks = createRemoteJWKSet(new URL("https://api.1pass.dev/.well-known/jwks.json"));

const { payload } = await jwtVerify(accessToken, jwks, {
  issuer: "https://api.1pass.dev",
  audience: "logi_a1b2c3d4...",  // my app's client_id
});
console.log(payload.sub);
ruby
require "jwt"
require "open-uri"

jwks_raw = URI.open("https://api.1pass.dev/.well-known/jwks.json").read
jwks = JWT::JWK::Set.new(JSON.parse(jwks_raw))

payload, _header = JWT.decode(
  access_token, nil, true,
  algorithms: ["RS256"],
  iss: "https://api.1pass.dev", verify_iss: true,
  aud: ENV["LOGI_CLIENT_ID"], verify_aud: true,
  jwks: jwks
)
python
import jwt
import requests

jwks = jwt.PyJWKClient("https://api.1pass.dev/.well-known/jwks.json")
signing_key = jwks.get_signing_key_from_jwt(access_token)
payload = jwt.decode(
    access_token, signing_key.key,
    algorithms=["RS256"],
    issuer="https://api.1pass.dev",
    audience="logi_a1b2c3d4...",
)
go
import "github.com/lestrrat-go/jwx/v2/jwk"

jwks, _ := jwk.Fetch(ctx, "https://api.1pass.dev/.well-known/jwks.json")
tok, err := jwt.Parse([]byte(accessToken),
    jwt.WithKeySet(jwks),
    jwt.WithIssuer("https://api.1pass.dev"),
    jwt.WithAudience("logi_a1b2c3d4..."),
)

After expiry

  • Once exp passes, you get an expired_token error
  • Call /oauth/token again with the refresh token to issue a new access token

If you need to check revocation immediately

Because a JWT is stateless, you cannot tell whether a jti has been revoked from self-verification alone. Use one of these:

  1. Opt-in — call /oauth/userinfo only on sensitive endpoints to re-verify with logi (with the 15-minute expiry, this is enough in most cases)
  2. Webhook — subscribe to the token.revoked event (logi → RP). On receiving a jwt_jti, add it to a local blocklist
  3. Introspection — call /oauth/introspect to check the active status directly

id_token (OIDC)

When the openid scope is requested, the /oauth/token response includes an id_token. Included claims:

  • iss, sub, aud, exp, iat, nonce (when requested)
  • at_hash — the left 128 bits of SHA256(access_token), base64url (OpenID Connect Core 1.0 §3.1.3.6)

Production issuer

The iss / issuer comparison value in all verification code is https://api.1pass.dev. This value matches exactly the issuer field of /.well-known/openid-configuration — we recommend that an RP fetch it from the discovery document and compare, rather than hardcoding it.

Verify the id_token the same way as the access token, but also compare nonce and at_hash (replay + token binding defense).

Identity가 제품의 신뢰를 만듭니다.