Add application sessions
This commit is contained in:
parent
a330ced1aa
commit
c5eef85e6b
16
.sqlx/query-0869ee80d31becfb73918a0cd4f3d45f6faa7bfcdf8f5e4e60ef0f7070f06504.json
generated
Normal file
16
.sqlx/query-0869ee80d31becfb73918a0cd4f3d45f6faa7bfcdf8f5e4e60ef0f7070f06504.json
generated
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO application_access_tokens (access_token_hash, session_id, issued_at_utc, expires_at_utc)\n VALUES ($1, $2, NOW(), $3)\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Bytea",
|
||||
"Int4",
|
||||
"Timestamp"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "0869ee80d31becfb73918a0cd4f3d45f6faa7bfcdf8f5e4e60ef0f7070f06504"
|
||||
}
|
23
.sqlx/query-8906051fcdb5602682a9beadeefd4dfa52961e7f683293fd334c24f14cffcac3.json
generated
Normal file
23
.sqlx/query-8906051fcdb5602682a9beadeefd4dfa52961e7f683293fd334c24f14cffcac3.json
generated
Normal file
@ -0,0 +1,23 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT 1 AS ok FROM login_sessions\n WHERE login_session_id = $1 AND user_id = $2\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "ok",
|
||||
"type_info": "Int4"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int4",
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "8906051fcdb5602682a9beadeefd4dfa52961e7f683293fd334c24f14cffcac3"
|
||||
}
|
14
.sqlx/query-9a4151f5594ff961abd8e615fabdf9a156c6d5e75b2f29db9c101381459cebfa.json
generated
Normal file
14
.sqlx/query-9a4151f5594ff961abd8e615fabdf9a156c6d5e75b2f29db9c101381459cebfa.json
generated
Normal file
@ -0,0 +1,14 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n DELETE FROM application_access_tokens WHERE access_token_hash = $1\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Bytea"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "9a4151f5594ff961abd8e615fabdf9a156c6d5e75b2f29db9c101381459cebfa"
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT user_name, user_id, csrf_secret\n FROM login_sessions INNER JOIN users USING (user_id)\n WHERE login_session_token_hash = $1\n ",
|
||||
"query": "\n SELECT user_name, user_id, login_session_id, csrf_secret\n FROM login_sessions INNER JOIN users USING (user_id)\n WHERE login_session_token_hash = $1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@ -15,6 +15,11 @@
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "login_session_id",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "csrf_secret",
|
||||
"type_info": "Bytea"
|
||||
}
|
||||
@ -25,10 +30,11 @@
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "7a2835e1bed065bddf7cec4a5a6740664e29e4faf414e4546e24c909a0bdfa67"
|
||||
"hash": "d6b173d90cd227a65bfc4a47ed3ba7b19650ed4d3dc44c1519775a6d0c9026b7"
|
||||
}
|
23
.sqlx/query-fd60725a6f572aaf53a2fb5d4163205703fc00aaeeedaa198b1a8d386faf9fb6.json
generated
Normal file
23
.sqlx/query-fd60725a6f572aaf53a2fb5d4163205703fc00aaeeedaa198b1a8d386faf9fb6.json
generated
Normal file
@ -0,0 +1,23 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO application_sessions (user_id, application_id, started_at_utc, last_seen_at_utc)\n VALUES ($1, $2, NOW(), NOW())\n RETURNING session_id\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "session_id",
|
||||
"type_info": "Int4"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "fd60725a6f572aaf53a2fb5d4163205703fc00aaeeedaa198b1a8d386faf9fb6"
|
||||
}
|
@ -8,6 +8,8 @@ CREATE TABLE login_sessions (
|
||||
csrf_secret BYTEA NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX ON login_sessions (user_id);
|
||||
|
||||
COMMENT ON TABLE login_sessions IS 'A login session is equivalent to one browser session where the user has logged in. The session is identified by a cookie.';
|
||||
|
||||
COMMENT ON COLUMN login_sessions.login_session_id IS 'Synthetic numeric ID for the session, used for cross-referencing only. Should not be public.';
|
||||
|
70
migrations/20240114144634_application_sessions.sql
Normal file
70
migrations/20240114144634_application_sessions.sql
Normal file
@ -0,0 +1,70 @@
|
||||
|
||||
CREATE TABLE application_sessions (
|
||||
session_id SERIAL NOT NULL PRIMARY KEY,
|
||||
user_id UUID NOT NULL REFERENCES users(user_id),
|
||||
application_id TEXT NOT NULL,
|
||||
|
||||
started_at_utc TIMESTAMP NOT NULL,
|
||||
last_seen_at_utc TIMESTAMP NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX ON application_sessions (user_id);
|
||||
|
||||
COMMENT ON TABLE application_sessions IS 'Stores the sessions that involve an OIDC application and a user.';
|
||||
|
||||
COMMENT ON COLUMN application_sessions.user_id IS 'UUID of the user.';
|
||||
|
||||
COMMENT ON COLUMN application_sessions.application_id IS 'client_id of the OpenID Connect application for which this session is issued.';
|
||||
|
||||
COMMENT ON COLUMN application_sessions.started_at_utc IS 'Time when this session was started.';
|
||||
|
||||
COMMENT ON COLUMN application_sessions.last_seen_at_utc IS 'Time when we last heard of activity in this session. Does not mean the user has not been using the application since.';
|
||||
|
||||
|
||||
CREATE TABLE application_access_tokens (
|
||||
access_token_hash BYTEA NOT NULL PRIMARY KEY,
|
||||
session_id INTEGER NOT NULL REFERENCES application_sessions(session_id) ON DELETE CASCADE,
|
||||
issued_at_utc TIMESTAMP NOT NULL,
|
||||
|
||||
expires_at_utc TIMESTAMP NOT NULL,
|
||||
|
||||
created_from_refresh_token_hash BYTEA UNIQUE -- intentionally nullable
|
||||
);
|
||||
|
||||
CREATE INDEX ON application_access_tokens (session_id);
|
||||
|
||||
COMMENT ON TABLE application_access_tokens IS 'Stores the hash of access tokens that have been issued to applications on the user''s behalf.';
|
||||
|
||||
COMMENT ON COLUMN application_access_tokens.access_token_hash IS 'BLAKE2s-256 hash of the refresh token.';
|
||||
|
||||
COMMENT ON COLUMN application_access_tokens.session_id IS 'ID of the session. This access token will cease to exist if the session is deleted.';
|
||||
|
||||
COMMENT ON COLUMN application_access_tokens.created_from_refresh_token_hash IS 'If set, this is the hash of the refresh token that was used to create this access token. This lets us track acknowledgement of a refresh.';
|
||||
|
||||
COMMENT ON COLUMN application_access_tokens.issued_at_utc IS 'Time when this access token was issued.';
|
||||
|
||||
COMMENT ON COLUMN application_access_tokens.expires_at_utc IS 'Time when this access token expires.';
|
||||
|
||||
|
||||
CREATE TABLE application_refresh_tokens (
|
||||
refresh_token_hash BYTEA NOT NULL PRIMARY KEY,
|
||||
session_id INTEGER NOT NULL REFERENCES application_sessions(session_id) ON DELETE CASCADE,
|
||||
created_from_refresh_token_hash BYTEA UNIQUE, -- intentionally nullable
|
||||
|
||||
issued_at_utc TIMESTAMP NOT NULL,
|
||||
expires_at_utc TIMESTAMP NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX ON application_refresh_tokens (session_id);
|
||||
|
||||
COMMENT ON TABLE application_refresh_tokens IS 'Stores the hash of refresh tokens that have been issued to applications on the user''s behalf.';
|
||||
|
||||
COMMENT ON COLUMN application_refresh_tokens.refresh_token_hash IS 'BLAKE2s-256 hash of the refresh token.';
|
||||
|
||||
COMMENT ON COLUMN application_refresh_tokens.session_id IS 'ID of the session. This refresh token will cease to exist if the session is deleted.';
|
||||
|
||||
COMMENT ON COLUMN application_refresh_tokens.created_from_refresh_token_hash IS 'If set, this is the hash of the refresh token that was used to create this refresh token. This lets us track acknowledgement of a refresh.';
|
||||
|
||||
COMMENT ON COLUMN application_refresh_tokens.issued_at_utc IS 'Time when this refresh token was issued.';
|
||||
|
||||
COMMENT ON COLUMN application_refresh_tokens.expires_at_utc IS 'Time when this refresh token expires.';
|
80
src/store.rs
80
src/store.rs
@ -1,4 +1,6 @@
|
||||
use chrono::DateTime;
|
||||
use chrono::NaiveDateTime;
|
||||
use chrono::Utc;
|
||||
use eyre::eyre;
|
||||
use eyre::Context;
|
||||
use futures::future::BoxFuture;
|
||||
@ -6,8 +8,7 @@ use sqlx::{types::Uuid, Connection, PgPool, Postgres, Transaction};
|
||||
use tracing::error;
|
||||
|
||||
use crate::web::login::{
|
||||
LoginSession, LOGIN_SESSION_CSRF_SECRET_BYTES,
|
||||
LOGIN_SESSION_TOKEN_HASH_BYTES,
|
||||
LoginSession, LOGIN_SESSION_CSRF_SECRET_BYTES, LOGIN_SESSION_TOKEN_HASH_BYTES,
|
||||
};
|
||||
|
||||
/// Postgres-backed storage for IdCoop
|
||||
@ -83,13 +84,77 @@ pub struct IdCoopStoreTxn<'a, 'txn> {
|
||||
impl<'a, 'txn> IdCoopStoreTxn<'a, 'txn> {
|
||||
pub async fn invalidate_access_token_by_hash(
|
||||
&mut self,
|
||||
access_token_hash: String,
|
||||
access_token_hash: &[u8],
|
||||
) -> eyre::Result<()> {
|
||||
todo!()
|
||||
// TODO presumably we also want to do the same for refresh tokens
|
||||
sqlx::query!(
|
||||
"
|
||||
DELETE FROM application_access_tokens WHERE access_token_hash = $1
|
||||
",
|
||||
access_token_hash
|
||||
)
|
||||
.execute(&mut **self.txn)
|
||||
.await
|
||||
.context("failed to invalidate access token by hash")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn issue_access_token(&mut self, access_token_hash: String) -> eyre::Result<()> {
|
||||
error!("TODO: issue access token");
|
||||
/// Creates a new application session for the given (user, application) pair.
|
||||
/// Requires the provided user login session ID to be valid and still active.
|
||||
/// Returns a new application session ID, or None if the login session was not active.
|
||||
pub async fn create_application_session(
|
||||
&mut self,
|
||||
user_id: Uuid,
|
||||
application_id: &str,
|
||||
login_session_id: i32,
|
||||
) -> eyre::Result<Option<i32>> {
|
||||
let login_session_opt = sqlx::query!(
|
||||
"
|
||||
SELECT 1 AS ok FROM login_sessions
|
||||
WHERE login_session_id = $1 AND user_id = $2
|
||||
",
|
||||
login_session_id,
|
||||
user_id
|
||||
)
|
||||
.fetch_optional(&mut **self.txn)
|
||||
.await
|
||||
.context("failed to fetch login session when creating application session")?;
|
||||
let Some(_login_session) = login_session_opt else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let row = sqlx::query!(
|
||||
"
|
||||
INSERT INTO application_sessions (user_id, application_id, started_at_utc, last_seen_at_utc)
|
||||
VALUES ($1, $2, NOW(), NOW())
|
||||
RETURNING session_id
|
||||
",
|
||||
user_id,
|
||||
application_id
|
||||
)
|
||||
.fetch_one(&mut **self.txn)
|
||||
.await
|
||||
.context("failed to create application session")?;
|
||||
Ok(Some(row.session_id))
|
||||
}
|
||||
|
||||
pub async fn issue_access_token(
|
||||
&mut self,
|
||||
access_token_hash: &[u8],
|
||||
session_id: i32,
|
||||
expires_at: DateTime<Utc>,
|
||||
) -> eyre::Result<()> {
|
||||
let expires_at = expires_at.naive_utc();
|
||||
sqlx::query!(
|
||||
"
|
||||
INSERT INTO application_access_tokens (access_token_hash, session_id, issued_at_utc, expires_at_utc)
|
||||
VALUES ($1, $2, NOW(), $3)
|
||||
",
|
||||
access_token_hash, session_id, expires_at
|
||||
)
|
||||
.execute(&mut **self.txn)
|
||||
.await
|
||||
.context("failed to issue access token")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -184,7 +249,7 @@ impl<'a, 'txn> IdCoopStoreTxn<'a, 'txn> {
|
||||
) -> eyre::Result<Option<LoginSession>> {
|
||||
let row_opt = sqlx::query!(
|
||||
"
|
||||
SELECT user_name, user_id, csrf_secret
|
||||
SELECT user_name, user_id, login_session_id, csrf_secret
|
||||
FROM login_sessions INNER JOIN users USING (user_id)
|
||||
WHERE login_session_token_hash = $1
|
||||
",
|
||||
@ -199,6 +264,7 @@ impl<'a, 'txn> IdCoopStoreTxn<'a, 'txn> {
|
||||
Ok(Some(LoginSession {
|
||||
user_name: row.user_name,
|
||||
user_id: row.user_id,
|
||||
login_session_id: row.login_session_id,
|
||||
csrf_secret: row
|
||||
.csrf_secret
|
||||
.try_into()
|
||||
|
@ -40,6 +40,7 @@ pub const LOGIN_SESSION_CSRF_SECRET_BYTES: usize = 8;
|
||||
pub struct LoginSession {
|
||||
pub user_name: String,
|
||||
pub user_id: Uuid,
|
||||
pub login_session_id: i32,
|
||||
pub csrf_secret: [u8; LOGIN_SESSION_CSRF_SECRET_BYTES],
|
||||
}
|
||||
|
||||
|
@ -10,7 +10,6 @@ use axum_csrf::CsrfToken;
|
||||
|
||||
use eyre::{Context, ContextCompat};
|
||||
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing::{error, info};
|
||||
|
||||
@ -230,7 +229,7 @@ async fn show_consent_page(
|
||||
/// - query.request_uri has been validated as a safe redirect URI
|
||||
async fn process_authorisation(
|
||||
query: AuthorisationQuery,
|
||||
_login_session: LoginSession,
|
||||
login_session: LoginSession,
|
||||
client_id: String,
|
||||
_client_config: &OidcClientConfiguration,
|
||||
config: &Configuration,
|
||||
@ -259,7 +258,8 @@ async fn process_authorisation(
|
||||
.code_challenge_method
|
||||
.unwrap_or_else(|| String::from("plain")),
|
||||
code_challenge,
|
||||
// TODO this should be bound to the login session
|
||||
user_id: login_session.user_id,
|
||||
user_login_session_id: login_session.login_session_id,
|
||||
},
|
||||
0,
|
||||
);
|
||||
|
@ -8,6 +8,7 @@ use std::{
|
||||
|
||||
use base64::{display::Base64Display, prelude::BASE64_URL_SAFE_NO_PAD, Engine};
|
||||
use rand::Rng;
|
||||
use sqlx::types::Uuid;
|
||||
use tokio::sync::Notify;
|
||||
|
||||
/// Display shows the auth code as base64 (URL-safe non-padded).
|
||||
@ -15,7 +16,10 @@ use tokio::sync::Notify;
|
||||
#[derive(Clone, Hash, PartialEq, Eq, Ord, PartialOrd)]
|
||||
pub struct AuthCode([u8; 24]);
|
||||
|
||||
pub type AccessTokenHash = String;
|
||||
pub type AccessToken = [u8; 32];
|
||||
pub type AccessTokenHash = [u8; 32];
|
||||
pub type RefreshToken = [u8; 32];
|
||||
pub type RefreshTokenHash = [u8; 32];
|
||||
|
||||
impl Display for AuthCode {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
@ -52,7 +56,8 @@ pub struct AuthCodeBinding {
|
||||
pub client_id: String,
|
||||
pub code_challenge_method: String,
|
||||
pub code_challenge: String,
|
||||
// TODO info about the user...
|
||||
pub user_id: Uuid,
|
||||
pub user_login_session_id: i32,
|
||||
}
|
||||
|
||||
pub struct RedeemedAuthCode {
|
||||
|
@ -8,22 +8,28 @@ use axum::{
|
||||
Extension, Form, Json, TypedHeader,
|
||||
};
|
||||
use base64::{prelude::BASE64_URL_SAFE_NO_PAD, Engine};
|
||||
use blake2::Blake2s256;
|
||||
use chrono::{DateTime, Duration, Utc};
|
||||
use eyre::{bail, Context};
|
||||
use josekit::{
|
||||
jws::{alg::rsassa::RsassaJwsAlgorithm::Rs256, JwsHeader},
|
||||
jwt::JwtPayload,
|
||||
};
|
||||
use rand::{thread_rng, Rng};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::{Digest, Sha256};
|
||||
use subtle::ConstantTimeEq;
|
||||
use tracing::error;
|
||||
use tracing::{debug, error};
|
||||
|
||||
use crate::{
|
||||
config::{Configuration, SecretConfig},
|
||||
store::IdCoopStore,
|
||||
};
|
||||
|
||||
use super::ext_codes::{AuthCode, CodeRedemption, VolatileCodeStore};
|
||||
use super::ext_codes::{
|
||||
AccessToken, AccessTokenHash, AuthCode, CodeRedemption, RefreshToken, RefreshTokenHash,
|
||||
VolatileCodeStore,
|
||||
};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct TokenFormParams {
|
||||
@ -162,10 +168,12 @@ pub async fn oidc_token(
|
||||
// Create an access token but don't actually issue it yet:
|
||||
// This lets us store the hash of the access token against the redemption of the auth code,
|
||||
// so double redemptions can invalidate the access token appropriately.
|
||||
let access_token = String::new(); // TODO!!!
|
||||
let access_token_hash = String::new(); // TODO!!!
|
||||
let refresh_token_hash = String::new(); // TODO!!!
|
||||
let refresh_token = String::new(); // TODO!!!
|
||||
let access_token = thread_rng().gen::<AccessToken>();
|
||||
let access_token_b64 = BASE64_URL_SAFE_NO_PAD.encode(access_token);
|
||||
let access_token_hash: AccessTokenHash = Blake2s256::digest(&access_token).into();
|
||||
let refresh_token = thread_rng().gen::<RefreshToken>();
|
||||
let refresh_token_b64 = BASE64_URL_SAFE_NO_PAD.encode(refresh_token);
|
||||
let refresh_token_hash: RefreshTokenHash = Blake2s256::digest(&refresh_token).into();
|
||||
|
||||
// Redeem the auth code so we can check it and then maybe issue an access token.
|
||||
let binding = match code_store.redeem(&auth_code, access_token_hash.clone()) {
|
||||
@ -187,7 +195,7 @@ pub async fn oidc_token(
|
||||
if let Err(err) = store
|
||||
.txn(move |mut txn| {
|
||||
Box::pin(async move {
|
||||
txn.invalidate_access_token_by_hash(access_token_to_invalidate)
|
||||
txn.invalidate_access_token_by_hash(&access_token_to_invalidate)
|
||||
.await
|
||||
.context("failed to invalidate access token")
|
||||
})
|
||||
@ -246,22 +254,59 @@ pub async fn oidc_token(
|
||||
.into_response();
|
||||
}
|
||||
|
||||
// Issue access token
|
||||
if let Err(err) = store
|
||||
let application_id = binding.client_id.clone();
|
||||
let user_id = binding.user_id;
|
||||
let user_login_session_id = binding.user_login_session_id;
|
||||
|
||||
// Issue access token for a new session
|
||||
match store
|
||||
.txn(move |mut txn| {
|
||||
Box::pin(async move {
|
||||
txn.issue_access_token(access_token_hash)
|
||||
// TODO!!! User info
|
||||
let Some(session_id) = txn
|
||||
.create_application_session(user_id, &application_id, user_login_session_id)
|
||||
.await
|
||||
.context("issue_access_token")
|
||||
.context("create_application_session")? else {
|
||||
return Ok(Err(
|
||||
(
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(TokenError {
|
||||
code: TokenErrorCode::InvalidGrant,
|
||||
description: "Auth code has expired or was not valid.".to_owned(),
|
||||
}),
|
||||
).into_response()
|
||||
));
|
||||
};
|
||||
txn.issue_access_token(
|
||||
&access_token_hash,
|
||||
session_id,
|
||||
// TODO Support custom expiry, not 100 years
|
||||
Utc::now() + Duration::days(365 * 100),
|
||||
)
|
||||
.await
|
||||
.context("issue_access_token")?;
|
||||
|
||||
Ok(Ok(session_id))
|
||||
})
|
||||
})
|
||||
.await
|
||||
{
|
||||
error!("failed to issue access token: {err:?}");
|
||||
return (
|
||||
Ok(Ok(session_id)) => {
|
||||
debug!(
|
||||
"created application session {session_id:?} for {:?}",
|
||||
binding.client_id
|
||||
);
|
||||
}
|
||||
Ok(Err(web_err)) => {
|
||||
return web_err;
|
||||
}
|
||||
Err(err) => {
|
||||
error!("failed to issue access token: {err:?}");
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"database error when issuing access token; ask the server administrator to check server logs"
|
||||
).into_response();
|
||||
}
|
||||
}
|
||||
let id_token = IdToken {
|
||||
iss: config.oidc.issuer.clone(),
|
||||
@ -285,10 +330,10 @@ pub async fn oidc_token(
|
||||
(
|
||||
StatusCode::OK,
|
||||
Json(TokenSuccess {
|
||||
access_token,
|
||||
access_token: access_token_b64,
|
||||
token_type: "Bearer".to_owned(),
|
||||
refresh_token,
|
||||
expires_in: 60, // TODO
|
||||
refresh_token: refresh_token_b64,
|
||||
expires_in: 86400 * 365, // TODO
|
||||
// This assumes that we only support the OpenID scope at present.
|
||||
scope: "openid".to_owned(),
|
||||
id_token,
|
||||
|
Loading…
Reference in New Issue
Block a user