rotateRefreshToken static method

Future<TokenPair> rotateRefreshToken(
  1. Session session, {
  2. required String refreshToken,
  3. 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

static 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 RefreshTokenMalformedException();
  }

  var refreshTokenRow = await RefreshToken.db.findById(
    session,
    refreshTokenData.id,
    transaction: transaction,
  );

  if (refreshTokenRow == null ||
      !uint8ListAreEqual(
        Uint8List.sublistView(refreshTokenRow.fixedSecret),
        refreshTokenData.fixedSecret,
      )) {
    // If the `fixedSecret` does not match, we do not delete the refresh token.
    // (Since the `id` is encoded on every access token and thus might be known to 3rd parties or leak, even after the access token itself has expired.)
    throw RefreshTokenNotFoundException();
  }

  if (refreshTokenRow.isExpired) {
    await RefreshToken.db.deleteRow(
      session,
      refreshTokenRow,
      transaction: transaction,
    );

    throw RefreshTokenExpiredException();
  }

  if (!await _refreshTokenSecretHash.validateHash(
    secret: refreshTokenData.rotatingSecret,
    hash: Uint8List.sublistView(refreshTokenRow.rotatingSecretHash),
    salt: Uint8List.sublistView(refreshTokenRow.rotatingSecretSalt),
  )) {
    await RefreshToken.db.deleteRow(
      session,
      refreshTokenRow,
      transaction: transaction,
    );

    throw RefreshTokenInvalidSecretException();
  }

  final newSecret = _generateRefreshTokenRotatingSecret();
  final newHash = await _refreshTokenSecretHash.createHash(secret: newSecret);

  refreshTokenRow = await RefreshToken.db.updateRow(
    session,
    refreshTokenRow.copyWith(
      rotatingSecretHash: ByteData.sublistView(newHash.hash),
      rotatingSecretSalt: ByteData.sublistView(newHash.salt),
      lastUpdatedAt: clock.now(),
    ),
    transaction: transaction,
  );

  return TokenPair(
    refreshToken: RefreshTokenString.buildRefreshTokenString(
      refreshToken: refreshTokenRow,
      rotatingSecret: newSecret,
    ),
    accessToken: _jwtUtil.createJwt(refreshTokenRow),
  );
}