// "Sign in with ..." for any OpenID Connect provider: the visitor is sent off with state, nonce and
// PKCE, comes back with a code, and the ID token is believed only once its signature and claims
// are checked. /login and /callback are what a site has; /flow plays the visitor against a test
// provider, and the routes after it show tokens that must be refused.
//
//   OIDC_ISSUER=https://accounts.example.com OIDC_CLIENT_ID=... OIDC_CLIENT_SECRET=... quillo run oidc.qwe
//   then open http://localhost:8080/login

use "oidc.qwer"

let ISSUER = env("OIDC_ISSUER", "")
let CLIENT = {
    client_id: env("OIDC_CLIENT_ID", ""),
    client_secret: env("OIDC_CLIENT_SECRET", ""),
    redirect_uri: env("OIDC_REDIRECT_URI", "http://localhost:8080/callback"),
}

route GET /login {
    let s = oidc_start(ISSUER, CLIENT, {})?
    set_session({ state: s.state, nonce: s.nonce, code_verifier: s.code_verifier }, { max_age: "10m" })
    redirect(s.url)
}

route GET /callback {
    let done = oidc_finish(ISSUER, CLIENT, request.query, session(), {})
    if is_err(done) { end_session(); reply 401 done.error }
    set_session({ user: done.claims.sub })
    { welcome: done.userinfo.name, email: done.userinfo.email }
}

// A browser's part: go to the provider, take the redirect back. `hint` asks the test provider for a
// kind of answer; it is an ordinary login_hint to a real one.
fn visit(hint) {
    let s = oidc_start(ISSUER, CLIENT, { login_hint: hint })?
    let back = http.get(s.url, {}, { redirects: 0, public_only: false })?
    let at = back.headers.location
    if at == null { return err(`the provider answered ${back.status}, with no redirect`) }
    { query: oidc_query(at), saved: s }
}

fn finish(hint) {
    let v = visit(hint)?
    oidc_finish(ISSUER, CLIENT, v.query, v.saved, {})
}

route GET /metadata {
    let m = oidc_discover(ISSUER)?
    { issuer: m.issuer, has_keys: m.jwks_uri != null, wrong_issuer: oidc_discover(ISSUER + "/other").error }
}

route GET /flow/:hint {
    let r = finish(hint)?
    { sub: r.claims.sub, aud: r.claims.aud, name: r.userinfo.name, email: r.userinfo.email, alg: oidc_peek(r.tokens.id_token)?.header.alg, refreshed: oidc_refresh(ISSUER, CLIENT, r.tokens.refresh_token)?.token_type }
}

// The provider signed with a key the keys held here do not have: they are fetched once more.
route GET /rotated {
    finish("ada")?
    { sub: finish("rotated")?.claims.sub, unknown_kid_again: finish("unknown-kid").error }
}

// Every one of these is a provider, or someone in between, trying something on.
route GET /refused {
    let v = visit("ada")?
    {
        expired: finish("expired").error,
        audience: finish("other-audience").error,
        issuer: finish("other-issuer").error,
        nonce: finish("wrong-nonce").error,
        forged: finish("forged").error,
        alg_none: finish("alg-none").error,
        hs256: finish("hs256").error,
        at_hash: finish("wrong-at-hash").error,
        userinfo: finish("other-sub").error,
        denied: finish("denied").error,
        state: oidc_finish(ISSUER, CLIENT, merge(v.query, { state: "stolen" }), v.saved, {}).error,
        verifier: oidc_finish(ISSUER, CLIENT, v.query, merge(v.saved, { code_verifier: "wrong-verifier" }), {}).error,
        second_use: second_use("ada").error,
    }
}

fn second_use(hint) {
    let v = visit(hint)?
    oidc_finish(ISSUER, CLIENT, v.query, v.saved, {})?
    oidc_finish(ISSUER, CLIENT, v.query, v.saved, {})
}

// A program calling an API as itself: a token by the client credentials grant, which the API
// checks with oidc_verify_access (its audience, issuer, expiry and scope).
route GET /machine {
    let t = oidc_client_credentials(ISSUER, { ...CLIENT, scope: "orders:refund", audience: "orders" })?
    let claims = oidc_verify_access(ISSUER, "orders", t.access_token, { scope: "orders:refund" })?
    {
        client: claims.client_id,
        scopes: claims.scopes,
        lasts: t.expires_at > 0,
        billing: error(oidc_verify_access(ISSUER, "billing", t.access_token, {})),
        no_scope: error(oidc_verify_access(ISSUER, "orders", t.access_token, { scope: "orders:delete" })),
    }
}

route GET /logout = { url: oidc_logout_url(ISSUER, "token", "https://shop.example/", "xyz")? }
