Skip to content

Commit 4bf9821

Browse files
committed
Also check session as part of /api/auth/v1/status and add a (sub-optimal) async session validation to TS client's constructor when initialized with tokens.
1 parent f70a938 commit 4bf9821

File tree

3 files changed

+93
-29
lines changed

3 files changed

+93
-29
lines changed

examples/blog/web/src/layouts/Base.astro

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ const description = "TrailBase Example Blog Application";
3939
<meta name="title" content={title} />
4040
<meta name="description" content={description} />
4141

42+
{/* Disable favicon */}
43+
<link rel="icon" href="data:image/png;base64,iVBORw0KGgo=" />
44+
4245
<title>{title}</title>
4346

4447
<ClientRouter />

trailbase-assets/js/client/src/index.ts

Lines changed: 45 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -385,9 +385,29 @@ export class Client {
385385
this._client = new ThinClient(baseUrl ? new URL(baseUrl) : undefined);
386386
this._authChange = opts?.onAuthChange;
387387

388+
const tokens = opts?.tokens;
388389
// Note: this is a double assignment to _tokenState to ensure the linter
389390
// that it's really initialized in the constructor.
390-
this._tokenState = this.setTokenState(buildTokenState(opts?.tokens), true);
391+
this._tokenState = this.setTokenState(buildTokenState(tokens), true);
392+
393+
if (tokens?.refresh_token !== undefined) {
394+
// Validate session. This is currently async, which allows to initialize
395+
// a Client synchronously from invalid tokens. We may want to consider
396+
// offering a safer async initializer to avoid "racy" behavior. Especially,
397+
// when the auth token is valid while the session has already been closed.
398+
this.checkAuthStatus()
399+
.then((tokens) => {
400+
if (tokens === undefined) {
401+
// In this case, the auth state has changed, so we should invoke the callback.
402+
this.setTokenState(buildTokenState(undefined), false);
403+
} else {
404+
// In this case, the auth state has remained the same, we're merely
405+
// updating the reminted auth token.
406+
this.setTokenState(buildTokenState(tokens), true);
407+
}
408+
})
409+
.catch(console.error);
410+
}
391411
}
392412

393413
public static init(site?: URL | string, opts?: ClientOptions): Client {
@@ -488,20 +508,31 @@ export class Client {
488508
});
489509
}
490510

491-
public async checkCookies(): Promise<Tokens | undefined> {
492-
const response = await this.fetch(`${authApiBasePath}/status`);
493-
const status: LoginStatusResponse = await response.json();
494-
495-
const authToken = status?.auth_token;
496-
if (authToken) {
497-
const newState = buildTokenState({
498-
auth_token: authToken,
499-
refresh_token: status.refresh_token,
500-
csrf_token: status.csrf_token,
501-
});
511+
/// This will call the status endpoint, which validates any provided tokens
512+
/// but also hoists any tokens provided as cookies into a JSON response.
513+
private async checkAuthStatus(): Promise<Tokens | undefined> {
514+
const response = await this.fetch(`${authApiBasePath}/status`, {
515+
throwOnError: false,
516+
});
517+
if (response.ok) {
518+
const status: LoginStatusResponse = await response.json();
519+
const auth_token = status.auth_token;
520+
if (auth_token) {
521+
return {
522+
auth_token,
523+
refresh_token: status.refresh_token,
524+
csrf_token: status.csrf_token,
525+
};
526+
}
527+
}
528+
return undefined;
529+
}
502530

531+
public async checkCookies(): Promise<Tokens | undefined> {
532+
const tokens = await this.checkAuthStatus();
533+
if (tokens) {
534+
const newState = buildTokenState(tokens);
503535
this.setTokenState(newState);
504-
505536
return newState.state?.tokens;
506537
}
507538
}
@@ -579,7 +610,7 @@ export class Client {
579610
return response;
580611
} catch (err) {
581612
if (err instanceof TypeError) {
582-
throw Error(`Connection refused ${err}. TrailBase down or CORS?`);
613+
console.debug(`Connection refused ${err}. TrailBase down or CORS?`);
583614
}
584615
throw err;
585616
}

trailbase-core/src/auth/api/login.rs

Lines changed: 45 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ use utoipa::{IntoParams, ToSchema};
1313
use crate::app_state::AppState;
1414
use crate::auth::AuthError;
1515
use crate::auth::password::check_user_password;
16-
use crate::auth::tokens::{Tokens, mint_new_tokens};
16+
use crate::auth::tokens::{Tokens, mint_new_tokens, reauth_with_refresh_token};
1717
use crate::auth::user::DbUser;
1818
use crate::auth::util::{
1919
new_cookie, remove_cookie, user_by_email, validate_and_normalize_email_address,
@@ -206,29 +206,59 @@ pub(crate) async fn login_status_handler(
206206
State(state): State<AppState>,
207207
tokens: Option<Tokens>,
208208
) -> Result<Json<LoginStatusResponse>, AuthError> {
209-
let Some(tokens) = tokens else {
209+
let Some(Tokens {
210+
auth_token_claims,
211+
refresh_token,
212+
}) = tokens
213+
else {
214+
// Return Ok but all Nones.
210215
return Ok(Json(LoginStatusResponse {
211216
auth_token: None,
212217
refresh_token: None,
213218
csrf_token: None,
214219
}));
215220
};
216221

217-
let Tokens {
218-
auth_token_claims,
219-
refresh_token,
220-
} = tokens;
222+
// Decoding the auth token into its claims, already validated the therein contained expiration
223+
// time (exp). But rather than just re-encoding it, we refresh it. This ensures that the
224+
// session is still alive.
225+
if let Some(refresh_token) = refresh_token {
226+
let (auth_token_ttl, refresh_token_ttl) = state.access_config(|c| c.auth.token_ttls());
227+
let claims = reauth_with_refresh_token(
228+
&state,
229+
refresh_token.clone(),
230+
refresh_token_ttl,
231+
auth_token_ttl,
232+
)
233+
.await?;
221234

222-
let auth_token = state
223-
.jwt()
224-
.encode(&auth_token_claims)
225-
.map_err(|err| AuthError::Internal(err.into()))?;
235+
let auth_token = state
236+
.jwt()
237+
.encode(&claims)
238+
.map_err(|err| AuthError::Internal(err.into()))?;
226239

227-
return Ok(Json(LoginStatusResponse {
228-
auth_token: Some(auth_token),
229-
refresh_token,
230-
csrf_token: Some(auth_token_claims.csrf_token),
231-
}));
240+
return Ok(Json(LoginStatusResponse {
241+
auth_token: Some(auth_token),
242+
refresh_token: Some(refresh_token),
243+
csrf_token: Some(claims.csrf_token),
244+
}));
245+
} else {
246+
// Fall back case: we don't have a refresh token so we cannot validate if a session is still
247+
// alive. We could look-up sessions by user id, however there can be more than one session
248+
// per user. Right now we return an OK with the original, re-encoded token. It may also make
249+
// sense to return an error here, i.e. consider this entire API more of a session status rather
250+
// than a token status.
251+
let auth_token = state
252+
.jwt()
253+
.encode(&auth_token_claims)
254+
.map_err(|err| AuthError::Internal(err.into()))?;
255+
256+
return Ok(Json(LoginStatusResponse {
257+
auth_token: Some(auth_token),
258+
refresh_token: None,
259+
csrf_token: Some(auth_token_claims.csrf_token),
260+
}));
261+
}
232262
}
233263

234264
pub struct NewTokens {

0 commit comments

Comments
 (0)