rotateRefreshToken method
Future<TokenPair>
rotateRefreshToken(
- Session session, {
- required String refreshToken,
- Transaction? transaction,
Returns a new refresh / access token pair.
This invalidates the previous refresh token. Previously created access tokens for this refresh token will continue to work until they expire.
Implementation
Future<TokenPair> rotateRefreshToken(
final Session session, {
required final String refreshToken,
final Transaction? transaction,
}) async {
final RefreshTokenStringData refreshTokenData;
try {
refreshTokenData = RefreshTokenString.parseRefreshTokenString(
refreshToken,
);
} catch (e, stackTrace) {
session.log(
'Received malformed refresh token',
exception: e,
stackTrace: stackTrace,
level: LogLevel.debug,
);
throw RefreshTokenMalformedServerException();
}
var refreshTokenRow = await RefreshToken.db.findById(
session,
refreshTokenData.id,
transaction: transaction,
);
if (refreshTokenRow == null ||
!uint8ListAreEqual(
Uint8List.sublistView(refreshTokenRow.fixedSecret),
refreshTokenData.fixedSecret,
)) {
throw RefreshTokenNotFoundServerException();
}
if (refreshTokenRow.isExpired(_refreshTokenLifetime)) {
await RefreshToken.db.deleteRow(
session,
refreshTokenRow,
transaction: transaction,
);
throw RefreshTokenExpiredServerException(
refreshTokenId: refreshTokenRow.id!,
authUserId: refreshTokenRow.authUserId,
);
}
if (!await _refreshTokenSecretHash.validateHashFromBytes(
secret: refreshTokenData.rotatingSecret,
hashString: refreshTokenRow.rotatingSecretHash,
)) {
await RefreshToken.db.deleteRow(
session,
refreshTokenRow,
transaction: transaction,
);
throw RefreshTokenInvalidSecretServerException(
refreshTokenId: refreshTokenRow.id!,
authUserId: refreshTokenRow.authUserId,
);
}
// Checked only once the caller has proven possession of the refresh token,
// so this does not become an oracle for the state of an account whose
// token the caller does not hold.
//
// A rotation re-establishes access for another full refresh token
// lifetime, so `blocked` has to be consulted here and not only in
// `createTokens` - otherwise blocking a user leaves any session they
// already hold running indefinitely. The token is deliberately left in
// place rather than deleted, so that lifting the block restores it.
//
// Read directly rather than through `AuthUsers.get`, which opens a
// transaction of its own. Rotations are not serialised, so nesting one
// here corrupts the savepoint stack when several run on the same session.
final authUser = await AuthUser.db.findById(
session,
refreshTokenRow.authUserId,
transaction: transaction,
);
if (authUser == null) {
throw AuthUserNotFoundException();
}
if (authUser.blocked) {
throw AuthUserBlockedException();
}
final newSecret = _generateRefreshTokenRotatingSecret();
final newHash = await _refreshTokenSecretHash.createHashFromBytes(
secret: newSecret,
);
refreshTokenRow = await RefreshToken.db.updateRow(
session,
refreshTokenRow.copyWith(
rotatingSecretHash: newHash,
lastUpdatedAt: clock.now(),
),
transaction: transaction,
);
return TokenPair(
refreshToken: RefreshTokenString.buildRefreshTokenString(
refreshToken: refreshTokenRow,
rotatingSecret: newSecret,
),
accessToken: _jwtUtil.createJwt(refreshTokenRow),
);
}