diff --git a/i18n/en.json b/i18n/en.json
index e4fc825cda..578fe9a115 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -1142,6 +1142,7 @@
   "location_picker_latitude_hint": "Enter your latitude here",
   "location_picker_longitude_error": "Enter a valid longitude",
   "location_picker_longitude_hint": "Enter your longitude here",
+  "lock": "Lock",
   "locked_folder": "Locked Folder",
   "log_out": "Log out",
   "log_out_all_devices": "Log Out All Devices",
diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md
index 9544b2ddab..620fc97664 100644
--- a/mobile/openapi/README.md
+++ b/mobile/openapi/README.md
@@ -111,13 +111,14 @@ Class | Method | HTTP request | Description
 *AuthenticationApi* | [**changePassword**](doc//AuthenticationApi.md#changepassword) | **POST** /auth/change-password | 
 *AuthenticationApi* | [**changePinCode**](doc//AuthenticationApi.md#changepincode) | **PUT** /auth/pin-code | 
 *AuthenticationApi* | [**getAuthStatus**](doc//AuthenticationApi.md#getauthstatus) | **GET** /auth/status | 
+*AuthenticationApi* | [**lockAuthSession**](doc//AuthenticationApi.md#lockauthsession) | **POST** /auth/session/lock | 
 *AuthenticationApi* | [**login**](doc//AuthenticationApi.md#login) | **POST** /auth/login | 
 *AuthenticationApi* | [**logout**](doc//AuthenticationApi.md#logout) | **POST** /auth/logout | 
 *AuthenticationApi* | [**resetPinCode**](doc//AuthenticationApi.md#resetpincode) | **DELETE** /auth/pin-code | 
 *AuthenticationApi* | [**setupPinCode**](doc//AuthenticationApi.md#setuppincode) | **POST** /auth/pin-code | 
 *AuthenticationApi* | [**signUpAdmin**](doc//AuthenticationApi.md#signupadmin) | **POST** /auth/admin-sign-up | 
+*AuthenticationApi* | [**unlockAuthSession**](doc//AuthenticationApi.md#unlockauthsession) | **POST** /auth/session/unlock | 
 *AuthenticationApi* | [**validateAccessToken**](doc//AuthenticationApi.md#validateaccesstoken) | **POST** /auth/validateToken | 
-*AuthenticationApi* | [**verifyPinCode**](doc//AuthenticationApi.md#verifypincode) | **POST** /auth/pin-code/verify | 
 *DeprecatedApi* | [**getRandom**](doc//DeprecatedApi.md#getrandom) | **GET** /assets/random | 
 *DownloadApi* | [**downloadArchive**](doc//DownloadApi.md#downloadarchive) | **POST** /download/archive | 
 *DownloadApi* | [**getDownloadInfo**](doc//DownloadApi.md#getdownloadinfo) | **POST** /download/info | 
@@ -198,6 +199,7 @@ Class | Method | HTTP request | Description
 *SessionsApi* | [**deleteAllSessions**](doc//SessionsApi.md#deleteallsessions) | **DELETE** /sessions | 
 *SessionsApi* | [**deleteSession**](doc//SessionsApi.md#deletesession) | **DELETE** /sessions/{id} | 
 *SessionsApi* | [**getSessions**](doc//SessionsApi.md#getsessions) | **GET** /sessions | 
+*SessionsApi* | [**lockSession**](doc//SessionsApi.md#locksession) | **POST** /sessions/{id}/lock | 
 *SharedLinksApi* | [**addSharedLinkAssets**](doc//SharedLinksApi.md#addsharedlinkassets) | **PUT** /shared-links/{id}/assets | 
 *SharedLinksApi* | [**createSharedLink**](doc//SharedLinksApi.md#createsharedlink) | **POST** /shared-links | 
 *SharedLinksApi* | [**getAllSharedLinks**](doc//SharedLinksApi.md#getallsharedlinks) | **GET** /shared-links | 
@@ -392,6 +394,7 @@ Class | Method | HTTP request | Description
  - [PersonUpdateDto](doc//PersonUpdateDto.md)
  - [PersonWithFacesResponseDto](doc//PersonWithFacesResponseDto.md)
  - [PinCodeChangeDto](doc//PinCodeChangeDto.md)
+ - [PinCodeResetDto](doc//PinCodeResetDto.md)
  - [PinCodeSetupDto](doc//PinCodeSetupDto.md)
  - [PlacesResponseDto](doc//PlacesResponseDto.md)
  - [PurchaseResponse](doc//PurchaseResponse.md)
@@ -424,6 +427,7 @@ Class | Method | HTTP request | Description
  - [SessionCreateDto](doc//SessionCreateDto.md)
  - [SessionCreateResponseDto](doc//SessionCreateResponseDto.md)
  - [SessionResponseDto](doc//SessionResponseDto.md)
+ - [SessionUnlockDto](doc//SessionUnlockDto.md)
  - [SharedLinkCreateDto](doc//SharedLinkCreateDto.md)
  - [SharedLinkEditDto](doc//SharedLinkEditDto.md)
  - [SharedLinkResponseDto](doc//SharedLinkResponseDto.md)
diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart
index d0e39e0965..8710298d7d 100644
--- a/mobile/openapi/lib/api.dart
+++ b/mobile/openapi/lib/api.dart
@@ -189,6 +189,7 @@ part 'model/person_statistics_response_dto.dart';
 part 'model/person_update_dto.dart';
 part 'model/person_with_faces_response_dto.dart';
 part 'model/pin_code_change_dto.dart';
+part 'model/pin_code_reset_dto.dart';
 part 'model/pin_code_setup_dto.dart';
 part 'model/places_response_dto.dart';
 part 'model/purchase_response.dart';
@@ -221,6 +222,7 @@ part 'model/server_version_response_dto.dart';
 part 'model/session_create_dto.dart';
 part 'model/session_create_response_dto.dart';
 part 'model/session_response_dto.dart';
+part 'model/session_unlock_dto.dart';
 part 'model/shared_link_create_dto.dart';
 part 'model/shared_link_edit_dto.dart';
 part 'model/shared_link_response_dto.dart';
diff --git a/mobile/openapi/lib/api/authentication_api.dart b/mobile/openapi/lib/api/authentication_api.dart
index 446a0616ed..5482a9fc51 100644
--- a/mobile/openapi/lib/api/authentication_api.dart
+++ b/mobile/openapi/lib/api/authentication_api.dart
@@ -143,6 +143,39 @@ class AuthenticationApi {
     return null;
   }
 
+  /// Performs an HTTP 'POST /auth/session/lock' operation and returns the [Response].
+  Future<Response> lockAuthSessionWithHttpInfo() async {
+    // ignore: prefer_const_declarations
+    final apiPath = r'/auth/session/lock';
+
+    // ignore: prefer_final_locals
+    Object? postBody;
+
+    final queryParams = <QueryParam>[];
+    final headerParams = <String, String>{};
+    final formParams = <String, String>{};
+
+    const contentTypes = <String>[];
+
+
+    return apiClient.invokeAPI(
+      apiPath,
+      'POST',
+      queryParams,
+      postBody,
+      headerParams,
+      formParams,
+      contentTypes.isEmpty ? null : contentTypes.first,
+    );
+  }
+
+  Future<void> lockAuthSession() async {
+    final response = await lockAuthSessionWithHttpInfo();
+    if (response.statusCode >= HttpStatus.badRequest) {
+      throw ApiException(response.statusCode, await _decodeBodyBytes(response));
+    }
+  }
+
   /// Performs an HTTP 'POST /auth/login' operation and returns the [Response].
   /// Parameters:
   ///
@@ -234,13 +267,13 @@ class AuthenticationApi {
   /// Performs an HTTP 'DELETE /auth/pin-code' operation and returns the [Response].
   /// Parameters:
   ///
-  /// * [PinCodeChangeDto] pinCodeChangeDto (required):
-  Future<Response> resetPinCodeWithHttpInfo(PinCodeChangeDto pinCodeChangeDto,) async {
+  /// * [PinCodeResetDto] pinCodeResetDto (required):
+  Future<Response> resetPinCodeWithHttpInfo(PinCodeResetDto pinCodeResetDto,) async {
     // ignore: prefer_const_declarations
     final apiPath = r'/auth/pin-code';
 
     // ignore: prefer_final_locals
-    Object? postBody = pinCodeChangeDto;
+    Object? postBody = pinCodeResetDto;
 
     final queryParams = <QueryParam>[];
     final headerParams = <String, String>{};
@@ -262,9 +295,9 @@ class AuthenticationApi {
 
   /// Parameters:
   ///
-  /// * [PinCodeChangeDto] pinCodeChangeDto (required):
-  Future<void> resetPinCode(PinCodeChangeDto pinCodeChangeDto,) async {
-    final response = await resetPinCodeWithHttpInfo(pinCodeChangeDto,);
+  /// * [PinCodeResetDto] pinCodeResetDto (required):
+  Future<void> resetPinCode(PinCodeResetDto pinCodeResetDto,) async {
+    final response = await resetPinCodeWithHttpInfo(pinCodeResetDto,);
     if (response.statusCode >= HttpStatus.badRequest) {
       throw ApiException(response.statusCode, await _decodeBodyBytes(response));
     }
@@ -356,6 +389,45 @@ class AuthenticationApi {
     return null;
   }
 
+  /// Performs an HTTP 'POST /auth/session/unlock' operation and returns the [Response].
+  /// Parameters:
+  ///
+  /// * [SessionUnlockDto] sessionUnlockDto (required):
+  Future<Response> unlockAuthSessionWithHttpInfo(SessionUnlockDto sessionUnlockDto,) async {
+    // ignore: prefer_const_declarations
+    final apiPath = r'/auth/session/unlock';
+
+    // ignore: prefer_final_locals
+    Object? postBody = sessionUnlockDto;
+
+    final queryParams = <QueryParam>[];
+    final headerParams = <String, String>{};
+    final formParams = <String, String>{};
+
+    const contentTypes = <String>['application/json'];
+
+
+    return apiClient.invokeAPI(
+      apiPath,
+      'POST',
+      queryParams,
+      postBody,
+      headerParams,
+      formParams,
+      contentTypes.isEmpty ? null : contentTypes.first,
+    );
+  }
+
+  /// Parameters:
+  ///
+  /// * [SessionUnlockDto] sessionUnlockDto (required):
+  Future<void> unlockAuthSession(SessionUnlockDto sessionUnlockDto,) async {
+    final response = await unlockAuthSessionWithHttpInfo(sessionUnlockDto,);
+    if (response.statusCode >= HttpStatus.badRequest) {
+      throw ApiException(response.statusCode, await _decodeBodyBytes(response));
+    }
+  }
+
   /// Performs an HTTP 'POST /auth/validateToken' operation and returns the [Response].
   Future<Response> validateAccessTokenWithHttpInfo() async {
     // ignore: prefer_const_declarations
@@ -396,43 +468,4 @@ class AuthenticationApi {
     }
     return null;
   }
-
-  /// Performs an HTTP 'POST /auth/pin-code/verify' operation and returns the [Response].
-  /// Parameters:
-  ///
-  /// * [PinCodeSetupDto] pinCodeSetupDto (required):
-  Future<Response> verifyPinCodeWithHttpInfo(PinCodeSetupDto pinCodeSetupDto,) async {
-    // ignore: prefer_const_declarations
-    final apiPath = r'/auth/pin-code/verify';
-
-    // ignore: prefer_final_locals
-    Object? postBody = pinCodeSetupDto;
-
-    final queryParams = <QueryParam>[];
-    final headerParams = <String, String>{};
-    final formParams = <String, String>{};
-
-    const contentTypes = <String>['application/json'];
-
-
-    return apiClient.invokeAPI(
-      apiPath,
-      'POST',
-      queryParams,
-      postBody,
-      headerParams,
-      formParams,
-      contentTypes.isEmpty ? null : contentTypes.first,
-    );
-  }
-
-  /// Parameters:
-  ///
-  /// * [PinCodeSetupDto] pinCodeSetupDto (required):
-  Future<void> verifyPinCode(PinCodeSetupDto pinCodeSetupDto,) async {
-    final response = await verifyPinCodeWithHttpInfo(pinCodeSetupDto,);
-    if (response.statusCode >= HttpStatus.badRequest) {
-      throw ApiException(response.statusCode, await _decodeBodyBytes(response));
-    }
-  }
 }
diff --git a/mobile/openapi/lib/api/sessions_api.dart b/mobile/openapi/lib/api/sessions_api.dart
index 9f850fb4c8..3228d31e91 100644
--- a/mobile/openapi/lib/api/sessions_api.dart
+++ b/mobile/openapi/lib/api/sessions_api.dart
@@ -179,4 +179,44 @@ class SessionsApi {
     }
     return null;
   }
+
+  /// Performs an HTTP 'POST /sessions/{id}/lock' operation and returns the [Response].
+  /// Parameters:
+  ///
+  /// * [String] id (required):
+  Future<Response> lockSessionWithHttpInfo(String id,) async {
+    // ignore: prefer_const_declarations
+    final apiPath = r'/sessions/{id}/lock'
+      .replaceAll('{id}', id);
+
+    // ignore: prefer_final_locals
+    Object? postBody;
+
+    final queryParams = <QueryParam>[];
+    final headerParams = <String, String>{};
+    final formParams = <String, String>{};
+
+    const contentTypes = <String>[];
+
+
+    return apiClient.invokeAPI(
+      apiPath,
+      'POST',
+      queryParams,
+      postBody,
+      headerParams,
+      formParams,
+      contentTypes.isEmpty ? null : contentTypes.first,
+    );
+  }
+
+  /// Parameters:
+  ///
+  /// * [String] id (required):
+  Future<void> lockSession(String id,) async {
+    final response = await lockSessionWithHttpInfo(id,);
+    if (response.statusCode >= HttpStatus.badRequest) {
+      throw ApiException(response.statusCode, await _decodeBodyBytes(response));
+    }
+  }
 }
diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart
index f40d09ecc3..a3b1c41ca6 100644
--- a/mobile/openapi/lib/api_client.dart
+++ b/mobile/openapi/lib/api_client.dart
@@ -434,6 +434,8 @@ class ApiClient {
           return PersonWithFacesResponseDto.fromJson(value);
         case 'PinCodeChangeDto':
           return PinCodeChangeDto.fromJson(value);
+        case 'PinCodeResetDto':
+          return PinCodeResetDto.fromJson(value);
         case 'PinCodeSetupDto':
           return PinCodeSetupDto.fromJson(value);
         case 'PlacesResponseDto':
@@ -498,6 +500,8 @@ class ApiClient {
           return SessionCreateResponseDto.fromJson(value);
         case 'SessionResponseDto':
           return SessionResponseDto.fromJson(value);
+        case 'SessionUnlockDto':
+          return SessionUnlockDto.fromJson(value);
         case 'SharedLinkCreateDto':
           return SharedLinkCreateDto.fromJson(value);
         case 'SharedLinkEditDto':
diff --git a/mobile/openapi/lib/model/auth_status_response_dto.dart b/mobile/openapi/lib/model/auth_status_response_dto.dart
index 0ccd87114e..4e823506ee 100644
--- a/mobile/openapi/lib/model/auth_status_response_dto.dart
+++ b/mobile/openapi/lib/model/auth_status_response_dto.dart
@@ -13,38 +13,70 @@ part of openapi.api;
 class AuthStatusResponseDto {
   /// Returns a new [AuthStatusResponseDto] instance.
   AuthStatusResponseDto({
+    this.expiresAt,
     required this.isElevated,
     required this.password,
     required this.pinCode,
+    this.pinExpiresAt,
   });
 
+  ///
+  /// Please note: This property should have been non-nullable! Since the specification file
+  /// does not include a default value (using the "default:" property), however, the generated
+  /// source code must fall back to having a nullable type.
+  /// Consider adding a "default:" property in the specification file to hide this note.
+  ///
+  String? expiresAt;
+
   bool isElevated;
 
   bool password;
 
   bool pinCode;
 
+  ///
+  /// Please note: This property should have been non-nullable! Since the specification file
+  /// does not include a default value (using the "default:" property), however, the generated
+  /// source code must fall back to having a nullable type.
+  /// Consider adding a "default:" property in the specification file to hide this note.
+  ///
+  String? pinExpiresAt;
+
   @override
   bool operator ==(Object other) => identical(this, other) || other is AuthStatusResponseDto &&
+    other.expiresAt == expiresAt &&
     other.isElevated == isElevated &&
     other.password == password &&
-    other.pinCode == pinCode;
+    other.pinCode == pinCode &&
+    other.pinExpiresAt == pinExpiresAt;
 
   @override
   int get hashCode =>
     // ignore: unnecessary_parenthesis
+    (expiresAt == null ? 0 : expiresAt!.hashCode) +
     (isElevated.hashCode) +
     (password.hashCode) +
-    (pinCode.hashCode);
+    (pinCode.hashCode) +
+    (pinExpiresAt == null ? 0 : pinExpiresAt!.hashCode);
 
   @override
-  String toString() => 'AuthStatusResponseDto[isElevated=$isElevated, password=$password, pinCode=$pinCode]';
+  String toString() => 'AuthStatusResponseDto[expiresAt=$expiresAt, isElevated=$isElevated, password=$password, pinCode=$pinCode, pinExpiresAt=$pinExpiresAt]';
 
   Map<String, dynamic> toJson() {
     final json = <String, dynamic>{};
+    if (this.expiresAt != null) {
+      json[r'expiresAt'] = this.expiresAt;
+    } else {
+    //  json[r'expiresAt'] = null;
+    }
       json[r'isElevated'] = this.isElevated;
       json[r'password'] = this.password;
       json[r'pinCode'] = this.pinCode;
+    if (this.pinExpiresAt != null) {
+      json[r'pinExpiresAt'] = this.pinExpiresAt;
+    } else {
+    //  json[r'pinExpiresAt'] = null;
+    }
     return json;
   }
 
@@ -57,9 +89,11 @@ class AuthStatusResponseDto {
       final json = value.cast<String, dynamic>();
 
       return AuthStatusResponseDto(
+        expiresAt: mapValueOfType<String>(json, r'expiresAt'),
         isElevated: mapValueOfType<bool>(json, r'isElevated')!,
         password: mapValueOfType<bool>(json, r'password')!,
         pinCode: mapValueOfType<bool>(json, r'pinCode')!,
+        pinExpiresAt: mapValueOfType<String>(json, r'pinExpiresAt'),
       );
     }
     return null;
diff --git a/mobile/openapi/lib/model/permission.dart b/mobile/openapi/lib/model/permission.dart
index 73ecbd5868..a85b5002bf 100644
--- a/mobile/openapi/lib/model/permission.dart
+++ b/mobile/openapi/lib/model/permission.dart
@@ -85,6 +85,7 @@ class Permission {
   static const sessionPeriodRead = Permission._(r'session.read');
   static const sessionPeriodUpdate = Permission._(r'session.update');
   static const sessionPeriodDelete = Permission._(r'session.delete');
+  static const sessionPeriodLock = Permission._(r'session.lock');
   static const sharedLinkPeriodCreate = Permission._(r'sharedLink.create');
   static const sharedLinkPeriodRead = Permission._(r'sharedLink.read');
   static const sharedLinkPeriodUpdate = Permission._(r'sharedLink.update');
@@ -171,6 +172,7 @@ class Permission {
     sessionPeriodRead,
     sessionPeriodUpdate,
     sessionPeriodDelete,
+    sessionPeriodLock,
     sharedLinkPeriodCreate,
     sharedLinkPeriodRead,
     sharedLinkPeriodUpdate,
@@ -292,6 +294,7 @@ class PermissionTypeTransformer {
         case r'session.read': return Permission.sessionPeriodRead;
         case r'session.update': return Permission.sessionPeriodUpdate;
         case r'session.delete': return Permission.sessionPeriodDelete;
+        case r'session.lock': return Permission.sessionPeriodLock;
         case r'sharedLink.create': return Permission.sharedLinkPeriodCreate;
         case r'sharedLink.read': return Permission.sharedLinkPeriodRead;
         case r'sharedLink.update': return Permission.sharedLinkPeriodUpdate;
diff --git a/mobile/openapi/lib/model/pin_code_reset_dto.dart b/mobile/openapi/lib/model/pin_code_reset_dto.dart
new file mode 100644
index 0000000000..3585348675
--- /dev/null
+++ b/mobile/openapi/lib/model/pin_code_reset_dto.dart
@@ -0,0 +1,125 @@
+//
+// AUTO-GENERATED FILE, DO NOT MODIFY!
+//
+// @dart=2.18
+
+// ignore_for_file: unused_element, unused_import
+// ignore_for_file: always_put_required_named_parameters_first
+// ignore_for_file: constant_identifier_names
+// ignore_for_file: lines_longer_than_80_chars
+
+part of openapi.api;
+
+class PinCodeResetDto {
+  /// Returns a new [PinCodeResetDto] instance.
+  PinCodeResetDto({
+    this.password,
+    this.pinCode,
+  });
+
+  ///
+  /// Please note: This property should have been non-nullable! Since the specification file
+  /// does not include a default value (using the "default:" property), however, the generated
+  /// source code must fall back to having a nullable type.
+  /// Consider adding a "default:" property in the specification file to hide this note.
+  ///
+  String? password;
+
+  ///
+  /// Please note: This property should have been non-nullable! Since the specification file
+  /// does not include a default value (using the "default:" property), however, the generated
+  /// source code must fall back to having a nullable type.
+  /// Consider adding a "default:" property in the specification file to hide this note.
+  ///
+  String? pinCode;
+
+  @override
+  bool operator ==(Object other) => identical(this, other) || other is PinCodeResetDto &&
+    other.password == password &&
+    other.pinCode == pinCode;
+
+  @override
+  int get hashCode =>
+    // ignore: unnecessary_parenthesis
+    (password == null ? 0 : password!.hashCode) +
+    (pinCode == null ? 0 : pinCode!.hashCode);
+
+  @override
+  String toString() => 'PinCodeResetDto[password=$password, pinCode=$pinCode]';
+
+  Map<String, dynamic> toJson() {
+    final json = <String, dynamic>{};
+    if (this.password != null) {
+      json[r'password'] = this.password;
+    } else {
+    //  json[r'password'] = null;
+    }
+    if (this.pinCode != null) {
+      json[r'pinCode'] = this.pinCode;
+    } else {
+    //  json[r'pinCode'] = null;
+    }
+    return json;
+  }
+
+  /// Returns a new [PinCodeResetDto] instance and imports its values from
+  /// [value] if it's a [Map], null otherwise.
+  // ignore: prefer_constructors_over_static_methods
+  static PinCodeResetDto? fromJson(dynamic value) {
+    upgradeDto(value, "PinCodeResetDto");
+    if (value is Map) {
+      final json = value.cast<String, dynamic>();
+
+      return PinCodeResetDto(
+        password: mapValueOfType<String>(json, r'password'),
+        pinCode: mapValueOfType<String>(json, r'pinCode'),
+      );
+    }
+    return null;
+  }
+
+  static List<PinCodeResetDto> listFromJson(dynamic json, {bool growable = false,}) {
+    final result = <PinCodeResetDto>[];
+    if (json is List && json.isNotEmpty) {
+      for (final row in json) {
+        final value = PinCodeResetDto.fromJson(row);
+        if (value != null) {
+          result.add(value);
+        }
+      }
+    }
+    return result.toList(growable: growable);
+  }
+
+  static Map<String, PinCodeResetDto> mapFromJson(dynamic json) {
+    final map = <String, PinCodeResetDto>{};
+    if (json is Map && json.isNotEmpty) {
+      json = json.cast<String, dynamic>(); // ignore: parameter_assignments
+      for (final entry in json.entries) {
+        final value = PinCodeResetDto.fromJson(entry.value);
+        if (value != null) {
+          map[entry.key] = value;
+        }
+      }
+    }
+    return map;
+  }
+
+  // maps a json object with a list of PinCodeResetDto-objects as value to a dart map
+  static Map<String, List<PinCodeResetDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
+    final map = <String, List<PinCodeResetDto>>{};
+    if (json is Map && json.isNotEmpty) {
+      // ignore: parameter_assignments
+      json = json.cast<String, dynamic>();
+      for (final entry in json.entries) {
+        map[entry.key] = PinCodeResetDto.listFromJson(entry.value, growable: growable,);
+      }
+    }
+    return map;
+  }
+
+  /// The list of required keys that must be present in a JSON.
+  static const requiredKeys = <String>{
+  };
+}
+
diff --git a/mobile/openapi/lib/model/session_create_response_dto.dart b/mobile/openapi/lib/model/session_create_response_dto.dart
index 1ef346c96a..ab1c4ca2d8 100644
--- a/mobile/openapi/lib/model/session_create_response_dto.dart
+++ b/mobile/openapi/lib/model/session_create_response_dto.dart
@@ -17,6 +17,7 @@ class SessionCreateResponseDto {
     required this.current,
     required this.deviceOS,
     required this.deviceType,
+    this.expiresAt,
     required this.id,
     required this.token,
     required this.updatedAt,
@@ -30,6 +31,14 @@ class SessionCreateResponseDto {
 
   String deviceType;
 
+  ///
+  /// Please note: This property should have been non-nullable! Since the specification file
+  /// does not include a default value (using the "default:" property), however, the generated
+  /// source code must fall back to having a nullable type.
+  /// Consider adding a "default:" property in the specification file to hide this note.
+  ///
+  String? expiresAt;
+
   String id;
 
   String token;
@@ -42,6 +51,7 @@ class SessionCreateResponseDto {
     other.current == current &&
     other.deviceOS == deviceOS &&
     other.deviceType == deviceType &&
+    other.expiresAt == expiresAt &&
     other.id == id &&
     other.token == token &&
     other.updatedAt == updatedAt;
@@ -53,12 +63,13 @@ class SessionCreateResponseDto {
     (current.hashCode) +
     (deviceOS.hashCode) +
     (deviceType.hashCode) +
+    (expiresAt == null ? 0 : expiresAt!.hashCode) +
     (id.hashCode) +
     (token.hashCode) +
     (updatedAt.hashCode);
 
   @override
-  String toString() => 'SessionCreateResponseDto[createdAt=$createdAt, current=$current, deviceOS=$deviceOS, deviceType=$deviceType, id=$id, token=$token, updatedAt=$updatedAt]';
+  String toString() => 'SessionCreateResponseDto[createdAt=$createdAt, current=$current, deviceOS=$deviceOS, deviceType=$deviceType, expiresAt=$expiresAt, id=$id, token=$token, updatedAt=$updatedAt]';
 
   Map<String, dynamic> toJson() {
     final json = <String, dynamic>{};
@@ -66,6 +77,11 @@ class SessionCreateResponseDto {
       json[r'current'] = this.current;
       json[r'deviceOS'] = this.deviceOS;
       json[r'deviceType'] = this.deviceType;
+    if (this.expiresAt != null) {
+      json[r'expiresAt'] = this.expiresAt;
+    } else {
+    //  json[r'expiresAt'] = null;
+    }
       json[r'id'] = this.id;
       json[r'token'] = this.token;
       json[r'updatedAt'] = this.updatedAt;
@@ -85,6 +101,7 @@ class SessionCreateResponseDto {
         current: mapValueOfType<bool>(json, r'current')!,
         deviceOS: mapValueOfType<String>(json, r'deviceOS')!,
         deviceType: mapValueOfType<String>(json, r'deviceType')!,
+        expiresAt: mapValueOfType<String>(json, r'expiresAt'),
         id: mapValueOfType<String>(json, r'id')!,
         token: mapValueOfType<String>(json, r'token')!,
         updatedAt: mapValueOfType<String>(json, r'updatedAt')!,
diff --git a/mobile/openapi/lib/model/session_response_dto.dart b/mobile/openapi/lib/model/session_response_dto.dart
index 92e2dc6067..cf9eb08a78 100644
--- a/mobile/openapi/lib/model/session_response_dto.dart
+++ b/mobile/openapi/lib/model/session_response_dto.dart
@@ -17,6 +17,7 @@ class SessionResponseDto {
     required this.current,
     required this.deviceOS,
     required this.deviceType,
+    this.expiresAt,
     required this.id,
     required this.updatedAt,
   });
@@ -29,6 +30,14 @@ class SessionResponseDto {
 
   String deviceType;
 
+  ///
+  /// Please note: This property should have been non-nullable! Since the specification file
+  /// does not include a default value (using the "default:" property), however, the generated
+  /// source code must fall back to having a nullable type.
+  /// Consider adding a "default:" property in the specification file to hide this note.
+  ///
+  String? expiresAt;
+
   String id;
 
   String updatedAt;
@@ -39,6 +48,7 @@ class SessionResponseDto {
     other.current == current &&
     other.deviceOS == deviceOS &&
     other.deviceType == deviceType &&
+    other.expiresAt == expiresAt &&
     other.id == id &&
     other.updatedAt == updatedAt;
 
@@ -49,11 +59,12 @@ class SessionResponseDto {
     (current.hashCode) +
     (deviceOS.hashCode) +
     (deviceType.hashCode) +
+    (expiresAt == null ? 0 : expiresAt!.hashCode) +
     (id.hashCode) +
     (updatedAt.hashCode);
 
   @override
-  String toString() => 'SessionResponseDto[createdAt=$createdAt, current=$current, deviceOS=$deviceOS, deviceType=$deviceType, id=$id, updatedAt=$updatedAt]';
+  String toString() => 'SessionResponseDto[createdAt=$createdAt, current=$current, deviceOS=$deviceOS, deviceType=$deviceType, expiresAt=$expiresAt, id=$id, updatedAt=$updatedAt]';
 
   Map<String, dynamic> toJson() {
     final json = <String, dynamic>{};
@@ -61,6 +72,11 @@ class SessionResponseDto {
       json[r'current'] = this.current;
       json[r'deviceOS'] = this.deviceOS;
       json[r'deviceType'] = this.deviceType;
+    if (this.expiresAt != null) {
+      json[r'expiresAt'] = this.expiresAt;
+    } else {
+    //  json[r'expiresAt'] = null;
+    }
       json[r'id'] = this.id;
       json[r'updatedAt'] = this.updatedAt;
     return json;
@@ -79,6 +95,7 @@ class SessionResponseDto {
         current: mapValueOfType<bool>(json, r'current')!,
         deviceOS: mapValueOfType<String>(json, r'deviceOS')!,
         deviceType: mapValueOfType<String>(json, r'deviceType')!,
+        expiresAt: mapValueOfType<String>(json, r'expiresAt'),
         id: mapValueOfType<String>(json, r'id')!,
         updatedAt: mapValueOfType<String>(json, r'updatedAt')!,
       );
diff --git a/mobile/openapi/lib/model/session_unlock_dto.dart b/mobile/openapi/lib/model/session_unlock_dto.dart
new file mode 100644
index 0000000000..4cfeb14385
--- /dev/null
+++ b/mobile/openapi/lib/model/session_unlock_dto.dart
@@ -0,0 +1,125 @@
+//
+// AUTO-GENERATED FILE, DO NOT MODIFY!
+//
+// @dart=2.18
+
+// ignore_for_file: unused_element, unused_import
+// ignore_for_file: always_put_required_named_parameters_first
+// ignore_for_file: constant_identifier_names
+// ignore_for_file: lines_longer_than_80_chars
+
+part of openapi.api;
+
+class SessionUnlockDto {
+  /// Returns a new [SessionUnlockDto] instance.
+  SessionUnlockDto({
+    this.password,
+    this.pinCode,
+  });
+
+  ///
+  /// Please note: This property should have been non-nullable! Since the specification file
+  /// does not include a default value (using the "default:" property), however, the generated
+  /// source code must fall back to having a nullable type.
+  /// Consider adding a "default:" property in the specification file to hide this note.
+  ///
+  String? password;
+
+  ///
+  /// Please note: This property should have been non-nullable! Since the specification file
+  /// does not include a default value (using the "default:" property), however, the generated
+  /// source code must fall back to having a nullable type.
+  /// Consider adding a "default:" property in the specification file to hide this note.
+  ///
+  String? pinCode;
+
+  @override
+  bool operator ==(Object other) => identical(this, other) || other is SessionUnlockDto &&
+    other.password == password &&
+    other.pinCode == pinCode;
+
+  @override
+  int get hashCode =>
+    // ignore: unnecessary_parenthesis
+    (password == null ? 0 : password!.hashCode) +
+    (pinCode == null ? 0 : pinCode!.hashCode);
+
+  @override
+  String toString() => 'SessionUnlockDto[password=$password, pinCode=$pinCode]';
+
+  Map<String, dynamic> toJson() {
+    final json = <String, dynamic>{};
+    if (this.password != null) {
+      json[r'password'] = this.password;
+    } else {
+    //  json[r'password'] = null;
+    }
+    if (this.pinCode != null) {
+      json[r'pinCode'] = this.pinCode;
+    } else {
+    //  json[r'pinCode'] = null;
+    }
+    return json;
+  }
+
+  /// Returns a new [SessionUnlockDto] instance and imports its values from
+  /// [value] if it's a [Map], null otherwise.
+  // ignore: prefer_constructors_over_static_methods
+  static SessionUnlockDto? fromJson(dynamic value) {
+    upgradeDto(value, "SessionUnlockDto");
+    if (value is Map) {
+      final json = value.cast<String, dynamic>();
+
+      return SessionUnlockDto(
+        password: mapValueOfType<String>(json, r'password'),
+        pinCode: mapValueOfType<String>(json, r'pinCode'),
+      );
+    }
+    return null;
+  }
+
+  static List<SessionUnlockDto> listFromJson(dynamic json, {bool growable = false,}) {
+    final result = <SessionUnlockDto>[];
+    if (json is List && json.isNotEmpty) {
+      for (final row in json) {
+        final value = SessionUnlockDto.fromJson(row);
+        if (value != null) {
+          result.add(value);
+        }
+      }
+    }
+    return result.toList(growable: growable);
+  }
+
+  static Map<String, SessionUnlockDto> mapFromJson(dynamic json) {
+    final map = <String, SessionUnlockDto>{};
+    if (json is Map && json.isNotEmpty) {
+      json = json.cast<String, dynamic>(); // ignore: parameter_assignments
+      for (final entry in json.entries) {
+        final value = SessionUnlockDto.fromJson(entry.value);
+        if (value != null) {
+          map[entry.key] = value;
+        }
+      }
+    }
+    return map;
+  }
+
+  // maps a json object with a list of SessionUnlockDto-objects as value to a dart map
+  static Map<String, List<SessionUnlockDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
+    final map = <String, List<SessionUnlockDto>>{};
+    if (json is Map && json.isNotEmpty) {
+      // ignore: parameter_assignments
+      json = json.cast<String, dynamic>();
+      for (final entry in json.entries) {
+        map[entry.key] = SessionUnlockDto.listFromJson(entry.value, growable: growable,);
+      }
+    }
+    return map;
+  }
+
+  /// The list of required keys that must be present in a JSON.
+  static const requiredKeys = <String>{
+  };
+}
+
diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json
index d4a1e219c9..89bdfef45e 100644
--- a/open-api/immich-openapi-specs.json
+++ b/open-api/immich-openapi-specs.json
@@ -2377,7 +2377,7 @@
           "content": {
             "application/json": {
               "schema": {
-                "$ref": "#/components/schemas/PinCodeChangeDto"
+                "$ref": "#/components/schemas/PinCodeResetDto"
               }
             }
           },
@@ -2470,15 +2470,40 @@
         ]
       }
     },
-    "/auth/pin-code/verify": {
+    "/auth/session/lock": {
       "post": {
-        "operationId": "verifyPinCode",
+        "operationId": "lockAuthSession",
+        "parameters": [],
+        "responses": {
+          "200": {
+            "description": ""
+          }
+        },
+        "security": [
+          {
+            "bearer": []
+          },
+          {
+            "cookie": []
+          },
+          {
+            "api_key": []
+          }
+        ],
+        "tags": [
+          "Authentication"
+        ]
+      }
+    },
+    "/auth/session/unlock": {
+      "post": {
+        "operationId": "unlockAuthSession",
         "parameters": [],
         "requestBody": {
           "content": {
             "application/json": {
               "schema": {
-                "$ref": "#/components/schemas/PinCodeSetupDto"
+                "$ref": "#/components/schemas/SessionUnlockDto"
               }
             }
           },
@@ -5695,6 +5720,41 @@
         ]
       }
     },
+    "/sessions/{id}/lock": {
+      "post": {
+        "operationId": "lockSession",
+        "parameters": [
+          {
+            "name": "id",
+            "required": true,
+            "in": "path",
+            "schema": {
+              "format": "uuid",
+              "type": "string"
+            }
+          }
+        ],
+        "responses": {
+          "204": {
+            "description": ""
+          }
+        },
+        "security": [
+          {
+            "bearer": []
+          },
+          {
+            "cookie": []
+          },
+          {
+            "api_key": []
+          }
+        ],
+        "tags": [
+          "Sessions"
+        ]
+      }
+    },
     "/shared-links": {
       "get": {
         "operationId": "getAllSharedLinks",
@@ -9327,6 +9387,9 @@
       },
       "AuthStatusResponseDto": {
         "properties": {
+          "expiresAt": {
+            "type": "string"
+          },
           "isElevated": {
             "type": "boolean"
           },
@@ -9335,6 +9398,9 @@
           },
           "pinCode": {
             "type": "boolean"
+          },
+          "pinExpiresAt": {
+            "type": "string"
           }
         },
         "required": [
@@ -11096,6 +11162,7 @@
           "session.read",
           "session.update",
           "session.delete",
+          "session.lock",
           "sharedLink.create",
           "sharedLink.read",
           "sharedLink.update",
@@ -11297,6 +11364,18 @@
         ],
         "type": "object"
       },
+      "PinCodeResetDto": {
+        "properties": {
+          "password": {
+            "type": "string"
+          },
+          "pinCode": {
+            "example": "123456",
+            "type": "string"
+          }
+        },
+        "type": "object"
+      },
       "PinCodeSetupDto": {
         "properties": {
           "pinCode": {
@@ -12109,6 +12188,9 @@
           "deviceType": {
             "type": "string"
           },
+          "expiresAt": {
+            "type": "string"
+          },
           "id": {
             "type": "string"
           },
@@ -12144,6 +12226,9 @@
           "deviceType": {
             "type": "string"
           },
+          "expiresAt": {
+            "type": "string"
+          },
           "id": {
             "type": "string"
           },
@@ -12161,6 +12246,18 @@
         ],
         "type": "object"
       },
+      "SessionUnlockDto": {
+        "properties": {
+          "password": {
+            "type": "string"
+          },
+          "pinCode": {
+            "example": "123456",
+            "type": "string"
+          }
+        },
+        "type": "object"
+      },
       "SharedLinkCreateDto": {
         "properties": {
           "albumId": {
diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts
index de0a723ffa..1d3a04da44 100644
--- a/open-api/typescript-sdk/src/fetch-client.ts
+++ b/open-api/typescript-sdk/src/fetch-client.ts
@@ -512,18 +512,28 @@ export type LogoutResponseDto = {
     redirectUri: string;
     successful: boolean;
 };
-export type PinCodeChangeDto = {
-    newPinCode: string;
+export type PinCodeResetDto = {
     password?: string;
     pinCode?: string;
 };
 export type PinCodeSetupDto = {
     pinCode: string;
 };
+export type PinCodeChangeDto = {
+    newPinCode: string;
+    password?: string;
+    pinCode?: string;
+};
+export type SessionUnlockDto = {
+    password?: string;
+    pinCode?: string;
+};
 export type AuthStatusResponseDto = {
+    expiresAt?: string;
     isElevated: boolean;
     password: boolean;
     pinCode: boolean;
+    pinExpiresAt?: string;
 };
 export type ValidateAccessTokenResponseDto = {
     authStatus: boolean;
@@ -1075,6 +1085,7 @@ export type SessionResponseDto = {
     current: boolean;
     deviceOS: string;
     deviceType: string;
+    expiresAt?: string;
     id: string;
     updatedAt: string;
 };
@@ -1089,6 +1100,7 @@ export type SessionCreateResponseDto = {
     current: boolean;
     deviceOS: string;
     deviceType: string;
+    expiresAt?: string;
     id: string;
     token: string;
     updatedAt: string;
@@ -2066,13 +2078,13 @@ export function logout(opts?: Oazapfts.RequestOpts) {
         method: "POST"
     }));
 }
-export function resetPinCode({ pinCodeChangeDto }: {
-    pinCodeChangeDto: PinCodeChangeDto;
+export function resetPinCode({ pinCodeResetDto }: {
+    pinCodeResetDto: PinCodeResetDto;
 }, opts?: Oazapfts.RequestOpts) {
     return oazapfts.ok(oazapfts.fetchText("/auth/pin-code", oazapfts.json({
         ...opts,
         method: "DELETE",
-        body: pinCodeChangeDto
+        body: pinCodeResetDto
     })));
 }
 export function setupPinCode({ pinCodeSetupDto }: {
@@ -2093,13 +2105,19 @@ export function changePinCode({ pinCodeChangeDto }: {
         body: pinCodeChangeDto
     })));
 }
-export function verifyPinCode({ pinCodeSetupDto }: {
-    pinCodeSetupDto: PinCodeSetupDto;
+export function lockAuthSession(opts?: Oazapfts.RequestOpts) {
+    return oazapfts.ok(oazapfts.fetchText("/auth/session/lock", {
+        ...opts,
+        method: "POST"
+    }));
+}
+export function unlockAuthSession({ sessionUnlockDto }: {
+    sessionUnlockDto: SessionUnlockDto;
 }, opts?: Oazapfts.RequestOpts) {
-    return oazapfts.ok(oazapfts.fetchText("/auth/pin-code/verify", oazapfts.json({
+    return oazapfts.ok(oazapfts.fetchText("/auth/session/unlock", oazapfts.json({
         ...opts,
         method: "POST",
-        body: pinCodeSetupDto
+        body: sessionUnlockDto
     })));
 }
 export function getAuthStatus(opts?: Oazapfts.RequestOpts) {
@@ -2952,6 +2970,14 @@ export function deleteSession({ id }: {
         method: "DELETE"
     }));
 }
+export function lockSession({ id }: {
+    id: string;
+}, opts?: Oazapfts.RequestOpts) {
+    return oazapfts.ok(oazapfts.fetchText(`/sessions/${encodeURIComponent(id)}/lock`, {
+        ...opts,
+        method: "POST"
+    }));
+}
 export function getAllSharedLinks({ albumId }: {
     albumId?: string;
 }, opts?: Oazapfts.RequestOpts) {
@@ -3709,6 +3735,7 @@ export enum Permission {
     SessionRead = "session.read",
     SessionUpdate = "session.update",
     SessionDelete = "session.delete",
+    SessionLock = "session.lock",
     SharedLinkCreate = "sharedLink.create",
     SharedLinkRead = "sharedLink.read",
     SharedLinkUpdate = "sharedLink.update",
diff --git a/server/src/controllers/auth.controller.ts b/server/src/controllers/auth.controller.ts
index 5d3ba8be95..78c611d761 100644
--- a/server/src/controllers/auth.controller.ts
+++ b/server/src/controllers/auth.controller.ts
@@ -9,7 +9,9 @@ import {
   LoginResponseDto,
   LogoutResponseDto,
   PinCodeChangeDto,
+  PinCodeResetDto,
   PinCodeSetupDto,
+  SessionUnlockDto,
   SignUpDto,
   ValidateAccessTokenResponseDto,
 } from 'src/dtos/auth.dto';
@@ -98,14 +100,21 @@ export class AuthController {
 
   @Delete('pin-code')
   @Authenticated()
-  async resetPinCode(@Auth() auth: AuthDto, @Body() dto: PinCodeChangeDto): Promise<void> {
+  async resetPinCode(@Auth() auth: AuthDto, @Body() dto: PinCodeResetDto): Promise<void> {
     return this.service.resetPinCode(auth, dto);
   }
 
-  @Post('pin-code/verify')
+  @Post('session/unlock')
   @HttpCode(HttpStatus.OK)
   @Authenticated()
-  async verifyPinCode(@Auth() auth: AuthDto, @Body() dto: PinCodeSetupDto): Promise<void> {
-    return this.service.verifyPinCode(auth, dto);
+  async unlockAuthSession(@Auth() auth: AuthDto, @Body() dto: SessionUnlockDto): Promise<void> {
+    return this.service.unlockSession(auth, dto);
+  }
+
+  @Post('session/lock')
+  @HttpCode(HttpStatus.OK)
+  @Authenticated()
+  async lockAuthSession(@Auth() auth: AuthDto): Promise<void> {
+    return this.service.lockSession(auth);
   }
 }
diff --git a/server/src/controllers/session.controller.ts b/server/src/controllers/session.controller.ts
index addcfd8fe9..3838d5af80 100644
--- a/server/src/controllers/session.controller.ts
+++ b/server/src/controllers/session.controller.ts
@@ -37,4 +37,11 @@ export class SessionController {
   deleteSession(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> {
     return this.service.delete(auth, id);
   }
+
+  @Post(':id/lock')
+  @Authenticated({ permission: Permission.SESSION_LOCK })
+  @HttpCode(HttpStatus.NO_CONTENT)
+  lockSession(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> {
+    return this.service.lock(auth, id);
+  }
 }
diff --git a/server/src/database.ts b/server/src/database.ts
index 29c746aa1f..cfccd70b75 100644
--- a/server/src/database.ts
+++ b/server/src/database.ts
@@ -232,6 +232,7 @@ export type Session = {
   id: string;
   createdAt: Date;
   updatedAt: Date;
+  expiresAt: Date | null;
   deviceOS: string;
   deviceType: string;
   pinExpiresAt: Date | null;
diff --git a/server/src/db.d.ts b/server/src/db.d.ts
index 6efbd5f7d7..943c9ddfa0 100644
--- a/server/src/db.d.ts
+++ b/server/src/db.d.ts
@@ -344,7 +344,7 @@ export interface Sessions {
   deviceType: Generated<string>;
   id: Generated<string>;
   parentId: string | null;
-  expiredAt: Date | null;
+  expiresAt: Date | null;
   token: string;
   updatedAt: Generated<Timestamp>;
   updateId: Generated<string>;
diff --git a/server/src/dtos/auth.dto.ts b/server/src/dtos/auth.dto.ts
index 8644426ab2..2f3ae5c14b 100644
--- a/server/src/dtos/auth.dto.ts
+++ b/server/src/dtos/auth.dto.ts
@@ -93,6 +93,8 @@ export class PinCodeResetDto {
   password?: string;
 }
 
+export class SessionUnlockDto extends PinCodeResetDto {}
+
 export class PinCodeChangeDto extends PinCodeResetDto {
   @PinCode()
   newPinCode!: string;
@@ -139,4 +141,6 @@ export class AuthStatusResponseDto {
   pinCode!: boolean;
   password!: boolean;
   isElevated!: boolean;
+  expiresAt?: string;
+  pinExpiresAt?: string;
 }
diff --git a/server/src/dtos/session.dto.ts b/server/src/dtos/session.dto.ts
index f109e44fa0..f15166fbf5 100644
--- a/server/src/dtos/session.dto.ts
+++ b/server/src/dtos/session.dto.ts
@@ -24,6 +24,7 @@ export class SessionResponseDto {
   id!: string;
   createdAt!: string;
   updatedAt!: string;
+  expiresAt?: string;
   current!: boolean;
   deviceType!: string;
   deviceOS!: string;
@@ -37,6 +38,7 @@ export const mapSession = (entity: Session, currentId?: string): SessionResponse
   id: entity.id,
   createdAt: entity.createdAt.toISOString(),
   updatedAt: entity.updatedAt.toISOString(),
+  expiresAt: entity.expiresAt?.toISOString(),
   current: currentId === entity.id,
   deviceOS: entity.deviceOS,
   deviceType: entity.deviceType,
diff --git a/server/src/enum.ts b/server/src/enum.ts
index c6feb27dcc..a4d2d21274 100644
--- a/server/src/enum.ts
+++ b/server/src/enum.ts
@@ -148,6 +148,7 @@ export enum Permission {
   SESSION_READ = 'session.read',
   SESSION_UPDATE = 'session.update',
   SESSION_DELETE = 'session.delete',
+  SESSION_LOCK = 'session.lock',
 
   SHARED_LINK_CREATE = 'sharedLink.create',
   SHARED_LINK_READ = 'sharedLink.read',
diff --git a/server/src/queries/access.repository.sql b/server/src/queries/access.repository.sql
index c73f44c19d..402bbdcfaf 100644
--- a/server/src/queries/access.repository.sql
+++ b/server/src/queries/access.repository.sql
@@ -199,6 +199,15 @@ where
   "partners"."sharedById" in ($1)
   and "partners"."sharedWithId" = $2
 
+-- AccessRepository.session.checkOwnerAccess
+select
+  "sessions"."id"
+from
+  "sessions"
+where
+  "sessions"."id" in ($1)
+  and "sessions"."userId" = $2
+
 -- AccessRepository.stack.checkOwnerAccess
 select
   "stacks"."id"
diff --git a/server/src/queries/session.repository.sql b/server/src/queries/session.repository.sql
index b265380a1f..6a9b69c2e3 100644
--- a/server/src/queries/session.repository.sql
+++ b/server/src/queries/session.repository.sql
@@ -1,12 +1,14 @@
 -- NOTE: This file is auto generated by ./sql-generator
 
--- SessionRepository.search
+-- SessionRepository.get
 select
-  *
+  "id",
+  "expiresAt",
+  "pinExpiresAt"
 from
   "sessions"
 where
-  "sessions"."updatedAt" <= $1
+  "id" = $1
 
 -- SessionRepository.getByToken
 select
@@ -37,8 +39,8 @@ from
 where
   "sessions"."token" = $1
   and (
-    "sessions"."expiredAt" is null
-    or "sessions"."expiredAt" > $2
+    "sessions"."expiresAt" is null
+    or "sessions"."expiresAt" > $2
   )
 
 -- SessionRepository.getByUserId
@@ -50,6 +52,10 @@ from
   and "users"."deletedAt" is null
 where
   "sessions"."userId" = $1
+  and (
+    "sessions"."expiresAt" is null
+    or "sessions"."expiresAt" > $2
+  )
 order by
   "sessions"."updatedAt" desc,
   "sessions"."createdAt" desc
@@ -58,3 +64,10 @@ order by
 delete from "sessions"
 where
   "id" = $1::uuid
+
+-- SessionRepository.lockAll
+update "sessions"
+set
+  "pinExpiresAt" = $1
+where
+  "userId" = $2
diff --git a/server/src/repositories/access.repository.ts b/server/src/repositories/access.repository.ts
index b25007c4ea..17f69c0e52 100644
--- a/server/src/repositories/access.repository.ts
+++ b/server/src/repositories/access.repository.ts
@@ -306,6 +306,25 @@ class NotificationAccess {
   }
 }
 
+class SessionAccess {
+  constructor(private db: Kysely<DB>) {}
+
+  @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
+  @ChunkedSet({ paramIndex: 1 })
+  async checkOwnerAccess(userId: string, sessionIds: Set<string>) {
+    if (sessionIds.size === 0) {
+      return new Set<string>();
+    }
+
+    return this.db
+      .selectFrom('sessions')
+      .select('sessions.id')
+      .where('sessions.id', 'in', [...sessionIds])
+      .where('sessions.userId', '=', userId)
+      .execute()
+      .then((sessions) => new Set(sessions.map((session) => session.id)));
+  }
+}
 class StackAccess {
   constructor(private db: Kysely<DB>) {}
 
@@ -456,6 +475,7 @@ export class AccessRepository {
   notification: NotificationAccess;
   person: PersonAccess;
   partner: PartnerAccess;
+  session: SessionAccess;
   stack: StackAccess;
   tag: TagAccess;
   timeline: TimelineAccess;
@@ -469,6 +489,7 @@ export class AccessRepository {
     this.notification = new NotificationAccess(db);
     this.person = new PersonAccess(db);
     this.partner = new PartnerAccess(db);
+    this.session = new SessionAccess(db);
     this.stack = new StackAccess(db);
     this.tag = new TagAccess(db);
     this.timeline = new TimelineAccess(db);
diff --git a/server/src/repositories/session.repository.ts b/server/src/repositories/session.repository.ts
index ce819470c7..6c3d10cb9a 100644
--- a/server/src/repositories/session.repository.ts
+++ b/server/src/repositories/session.repository.ts
@@ -20,20 +20,20 @@ export class SessionRepository {
       .where((eb) =>
         eb.or([
           eb('updatedAt', '<=', DateTime.now().minus({ days: 90 }).toJSDate()),
-          eb.and([eb('expiredAt', 'is not', null), eb('expiredAt', '<=', DateTime.now().toJSDate())]),
+          eb.and([eb('expiresAt', 'is not', null), eb('expiresAt', '<=', DateTime.now().toJSDate())]),
         ]),
       )
       .returning(['id', 'deviceOS', 'deviceType'])
       .execute();
   }
 
-  @GenerateSql({ params: [{ updatedBefore: DummyValue.DATE }] })
-  search(options: SessionSearchOptions) {
+  @GenerateSql({ params: [DummyValue.UUID] })
+  get(id: string) {
     return this.db
       .selectFrom('sessions')
-      .selectAll()
-      .where('sessions.updatedAt', '<=', options.updatedBefore)
-      .execute();
+      .select(['id', 'expiresAt', 'pinExpiresAt'])
+      .where('id', '=', id)
+      .executeTakeFirst();
   }
 
   @GenerateSql({ params: [DummyValue.STRING] })
@@ -52,7 +52,7 @@ export class SessionRepository {
       ])
       .where('sessions.token', '=', token)
       .where((eb) =>
-        eb.or([eb('sessions.expiredAt', 'is', null), eb('sessions.expiredAt', '>', DateTime.now().toJSDate())]),
+        eb.or([eb('sessions.expiresAt', 'is', null), eb('sessions.expiresAt', '>', DateTime.now().toJSDate())]),
       )
       .executeTakeFirst();
   }
@@ -64,6 +64,9 @@ export class SessionRepository {
       .innerJoin('users', (join) => join.onRef('users.id', '=', 'sessions.userId').on('users.deletedAt', 'is', null))
       .selectAll('sessions')
       .where('sessions.userId', '=', userId)
+      .where((eb) =>
+        eb.or([eb('sessions.expiresAt', 'is', null), eb('sessions.expiresAt', '>', DateTime.now().toJSDate())]),
+      )
       .orderBy('sessions.updatedAt', 'desc')
       .orderBy('sessions.createdAt', 'desc')
       .execute();
@@ -86,4 +89,9 @@ export class SessionRepository {
   async delete(id: string) {
     await this.db.deleteFrom('sessions').where('id', '=', asUuid(id)).execute();
   }
+
+  @GenerateSql({ params: [DummyValue.UUID] })
+  async lockAll(userId: string) {
+    await this.db.updateTable('sessions').set({ pinExpiresAt: null }).where('userId', '=', userId).execute();
+  }
 }
diff --git a/server/src/schema/migrations/1747338664832-SessionRename.ts b/server/src/schema/migrations/1747338664832-SessionRename.ts
new file mode 100644
index 0000000000..5ba532d136
--- /dev/null
+++ b/server/src/schema/migrations/1747338664832-SessionRename.ts
@@ -0,0 +1,9 @@
+import { Kysely, sql } from 'kysely';
+
+export async function up(db: Kysely<any>): Promise<void> {
+  await sql`ALTER TABLE "sessions" RENAME "expiredAt" TO "expiresAt";`.execute(db);
+}
+
+export async function down(db: Kysely<any>): Promise<void> {
+  await sql`ALTER TABLE "sessions" RENAME "expiresAt" TO "expiredAt";`.execute(db);
+}
diff --git a/server/src/schema/tables/session.table.ts b/server/src/schema/tables/session.table.ts
index 9cc41c5bba..6bd5d84cb2 100644
--- a/server/src/schema/tables/session.table.ts
+++ b/server/src/schema/tables/session.table.ts
@@ -26,7 +26,7 @@ export class SessionTable {
   updatedAt!: Date;
 
   @Column({ type: 'timestamp with time zone', nullable: true })
-  expiredAt!: Date | null;
+  expiresAt!: Date | null;
 
   @ForeignKeyColumn(() => UserTable, { onUpdate: 'CASCADE', onDelete: 'CASCADE' })
   userId!: string;
diff --git a/server/src/services/auth.service.spec.ts b/server/src/services/auth.service.spec.ts
index fb1a5ae042..4bc5f1ce0b 100644
--- a/server/src/services/auth.service.spec.ts
+++ b/server/src/services/auth.service.spec.ts
@@ -924,13 +924,13 @@ describe(AuthService.name, () => {
       const user = factory.userAdmin();
       mocks.user.getForPinCode.mockResolvedValue({ pinCode: '123456 (hashed)', password: '' });
       mocks.crypto.compareBcrypt.mockImplementation((a, b) => `${a} (hashed)` === b);
-      mocks.session.getByUserId.mockResolvedValue([currentSession]);
+      mocks.session.lockAll.mockResolvedValue(void 0);
       mocks.session.update.mockResolvedValue(currentSession);
 
       await sut.resetPinCode(factory.auth({ user }), { pinCode: '123456' });
 
       expect(mocks.user.update).toHaveBeenCalledWith(user.id, { pinCode: null });
-      expect(mocks.session.update).toHaveBeenCalledWith(currentSession.id, { pinExpiresAt: null });
+      expect(mocks.session.lockAll).toHaveBeenCalledWith(user.id);
     });
 
     it('should throw if the PIN code does not match', async () => {
diff --git a/server/src/services/auth.service.ts b/server/src/services/auth.service.ts
index 7bda2eeb98..e6c541a624 100644
--- a/server/src/services/auth.service.ts
+++ b/server/src/services/auth.service.ts
@@ -18,6 +18,7 @@ import {
   PinCodeChangeDto,
   PinCodeResetDto,
   PinCodeSetupDto,
+  SessionUnlockDto,
   SignUpDto,
   mapLoginResponse,
 } from 'src/dtos/auth.dto';
@@ -123,24 +124,21 @@ export class AuthService extends BaseService {
 
   async resetPinCode(auth: AuthDto, dto: PinCodeResetDto) {
     const user = await this.userRepository.getForPinCode(auth.user.id);
-    this.resetPinChecks(user, dto);
+    this.validatePinCode(user, dto);
 
     await this.userRepository.update(auth.user.id, { pinCode: null });
-    const sessions = await this.sessionRepository.getByUserId(auth.user.id);
-    for (const session of sessions) {
-      await this.sessionRepository.update(session.id, { pinExpiresAt: null });
-    }
+    await this.sessionRepository.lockAll(auth.user.id);
   }
 
   async changePinCode(auth: AuthDto, dto: PinCodeChangeDto) {
     const user = await this.userRepository.getForPinCode(auth.user.id);
-    this.resetPinChecks(user, dto);
+    this.validatePinCode(user, dto);
 
     const hashed = await this.cryptoRepository.hashBcrypt(dto.newPinCode, SALT_ROUNDS);
     await this.userRepository.update(auth.user.id, { pinCode: hashed });
   }
 
-  private resetPinChecks(
+  private validatePinCode(
     user: { pinCode: string | null; password: string | null },
     dto: { pinCode?: string; password?: string },
   ) {
@@ -474,23 +472,27 @@ export class AuthService extends BaseService {
     throw new UnauthorizedException('Invalid user token');
   }
 
-  async verifyPinCode(auth: AuthDto, dto: PinCodeSetupDto): Promise<void> {
-    const user = await this.userRepository.getForPinCode(auth.user.id);
-    if (!user) {
-      throw new UnauthorizedException();
-    }
-
-    this.resetPinChecks(user, { pinCode: dto.pinCode });
-
+  async unlockSession(auth: AuthDto, dto: SessionUnlockDto): Promise<void> {
     if (!auth.session) {
-      throw new BadRequestException('Session is missing');
+      throw new BadRequestException('This endpoint can only be used with a session token');
     }
 
+    const user = await this.userRepository.getForPinCode(auth.user.id);
+    this.validatePinCode(user, { pinCode: dto.pinCode });
+
     await this.sessionRepository.update(auth.session.id, {
-      pinExpiresAt: new Date(DateTime.now().plus({ minutes: 15 }).toJSDate()),
+      pinExpiresAt: DateTime.now().plus({ minutes: 15 }).toJSDate(),
     });
   }
 
+  async lockSession(auth: AuthDto): Promise<void> {
+    if (!auth.session) {
+      throw new BadRequestException('This endpoint can only be used with a session token');
+    }
+
+    await this.sessionRepository.update(auth.session.id, { pinExpiresAt: null });
+  }
+
   private async createLoginResponse(user: UserAdmin, loginDetails: LoginDetails) {
     const token = this.cryptoRepository.randomBytesAsText(32);
     const tokenHashed = this.cryptoRepository.hashSha256(token);
@@ -526,10 +528,14 @@ export class AuthService extends BaseService {
       throw new UnauthorizedException();
     }
 
+    const session = auth.session ? await this.sessionRepository.get(auth.session.id) : undefined;
+
     return {
       pinCode: !!user.pinCode,
       password: !!user.password,
       isElevated: !!auth.session?.hasElevatedPermission,
+      expiresAt: session?.expiresAt?.toISOString(),
+      pinExpiresAt: session?.pinExpiresAt?.toISOString(),
     };
   }
 }
diff --git a/server/src/services/session.service.ts b/server/src/services/session.service.ts
index 9f49cda07f..059ff00e16 100644
--- a/server/src/services/session.service.ts
+++ b/server/src/services/session.service.ts
@@ -30,7 +30,7 @@ export class SessionService extends BaseService {
     const session = await this.sessionRepository.create({
       parentId: auth.session.id,
       userId: auth.user.id,
-      expiredAt: dto.duration ? DateTime.now().plus({ seconds: dto.duration }).toJSDate() : null,
+      expiresAt: dto.duration ? DateTime.now().plus({ seconds: dto.duration }).toJSDate() : null,
       deviceType: dto.deviceType,
       deviceOS: dto.deviceOS,
       token: tokenHashed,
@@ -49,6 +49,11 @@ export class SessionService extends BaseService {
     await this.sessionRepository.delete(id);
   }
 
+  async lock(auth: AuthDto, id: string): Promise<void> {
+    await this.requireAccess({ auth, permission: Permission.SESSION_LOCK, ids: [id] });
+    await this.sessionRepository.update(id, { pinExpiresAt: null });
+  }
+
   async deleteAll(auth: AuthDto): Promise<void> {
     const sessions = await this.sessionRepository.getByUserId(auth.user.id);
     for (const session of sessions) {
diff --git a/server/src/utils/access.ts b/server/src/utils/access.ts
index e2fe7429f3..38697a654b 100644
--- a/server/src/utils/access.ts
+++ b/server/src/utils/access.ts
@@ -280,6 +280,13 @@ const checkOtherAccess = async (access: AccessRepository, request: OtherAccessRe
       return await access.partner.checkUpdateAccess(auth.user.id, ids);
     }
 
+    case Permission.SESSION_READ:
+    case Permission.SESSION_UPDATE:
+    case Permission.SESSION_DELETE:
+    case Permission.SESSION_LOCK: {
+      return access.session.checkOwnerAccess(auth.user.id, ids);
+    }
+
     case Permission.STACK_READ: {
       return access.stack.checkOwnerAccess(auth.user.id, ids);
     }
diff --git a/server/test/repositories/access.repository.mock.ts b/server/test/repositories/access.repository.mock.ts
index 5b98b95e27..50db983cba 100644
--- a/server/test/repositories/access.repository.mock.ts
+++ b/server/test/repositories/access.repository.mock.ts
@@ -50,6 +50,10 @@ export const newAccessRepositoryMock = (): IAccessRepositoryMock => {
       checkUpdateAccess: vitest.fn().mockResolvedValue(new Set()),
     },
 
+    session: {
+      checkOwnerAccess: vitest.fn().mockResolvedValue(new Set()),
+    },
+
     stack: {
       checkOwnerAccess: vitest.fn().mockResolvedValue(new Set()),
     },
diff --git a/server/test/small.factory.ts b/server/test/small.factory.ts
index 231deeba83..75e36c1da2 100644
--- a/server/test/small.factory.ts
+++ b/server/test/small.factory.ts
@@ -127,7 +127,7 @@ const sessionFactory = (session: Partial<Session> = {}) => ({
   deviceType: 'mobile',
   token: 'abc123',
   parentId: null,
-  expiredAt: null,
+  expiresAt: null,
   userId: newUuid(),
   pinExpiresAt: newDate(),
   ...session,
diff --git a/web/src/routes/(user)/locked/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/locked/[[photos=photos]]/[[assetId=id]]/+page.svelte
index 49b40866dd..9c41a7fe59 100644
--- a/web/src/routes/(user)/locked/[[photos=photos]]/[[assetId=id]]/+page.svelte
+++ b/web/src/routes/(user)/locked/[[photos=photos]]/[[assetId=id]]/+page.svelte
@@ -1,4 +1,5 @@
 <script lang="ts">
+  import { goto } from '$app/navigation';
   import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
   import ChangeDate from '$lib/components/photos-page/actions/change-date-action.svelte';
   import ChangeLocation from '$lib/components/photos-page/actions/change-location-action.svelte';
@@ -10,11 +11,12 @@
   import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte';
   import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
   import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte';
-  import { AssetAction } from '$lib/constants';
+  import { AppRoute, AssetAction } from '$lib/constants';
   import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
   import { AssetStore } from '$lib/stores/assets-store.svelte';
-  import { AssetVisibility } from '@immich/sdk';
-  import { mdiDotsVertical } from '@mdi/js';
+  import { AssetVisibility, lockAuthSession } from '@immich/sdk';
+  import { Button } from '@immich/ui';
+  import { mdiDotsVertical, mdiLockOutline } from '@mdi/js';
   import { onDestroy } from 'svelte';
   import { t } from 'svelte-i18n';
   import type { PageData } from './$types';
@@ -42,6 +44,11 @@
     assetInteraction.clearMultiselect();
     assetStore.removeAssets(assetIds);
   };
+
+  const handleLock = async () => {
+    await lockAuthSession();
+    await goto(AppRoute.PHOTOS);
+  };
 </script>
 
 <!-- Multi-selection mode app bar -->
@@ -62,6 +69,12 @@
 {/if}
 
 <UserPageLayout hideNavbar={assetInteraction.selectionActive} title={data.meta.title} scrollbar={false}>
+  {#snippet buttons()}
+    <Button size="small" variant="filled" color="warning" leadingIcon={mdiLockOutline} onclick={handleLock}>
+      {$t('lock')}
+    </Button>
+  {/snippet}
+
   <AssetGrid
     enableRouting={true}
     {assetStore}
diff --git a/web/src/routes/(user)/locked/[[photos=photos]]/[[assetId=id]]/+page.ts b/web/src/routes/(user)/locked/[[photos=photos]]/[[assetId=id]]/+page.ts
index 445917f0d0..cf2e415da0 100644
--- a/web/src/routes/(user)/locked/[[photos=photos]]/[[assetId=id]]/+page.ts
+++ b/web/src/routes/(user)/locked/[[photos=photos]]/[[assetId=id]]/+page.ts
@@ -8,14 +8,12 @@ import type { PageLoad } from './$types';
 
 export const load = (async ({ params, url }) => {
   await authenticate(url);
+
   const { isElevated, pinCode } = await getAuthStatus();
-
   if (!isElevated || !pinCode) {
-    const continuePath = encodeURIComponent(url.pathname);
-    const redirectPath = `${AppRoute.AUTH_PIN_PROMPT}?continue=${continuePath}`;
-
-    redirect(302, redirectPath);
+    redirect(302, `${AppRoute.AUTH_PIN_PROMPT}?continue=${encodeURIComponent(url.pathname + url.search)}`);
   }
+
   const asset = await getAssetInfoFromParam(params);
   const $t = await getFormatter();
 
diff --git a/web/src/routes/auth/pin-prompt/+page.svelte b/web/src/routes/auth/pin-prompt/+page.svelte
index 91480cd35c..ffed9d5de0 100644
--- a/web/src/routes/auth/pin-prompt/+page.svelte
+++ b/web/src/routes/auth/pin-prompt/+page.svelte
@@ -3,9 +3,8 @@
   import AuthPageLayout from '$lib/components/layouts/AuthPageLayout.svelte';
   import PinCodeCreateForm from '$lib/components/user-settings-page/PinCodeCreateForm.svelte';
   import PincodeInput from '$lib/components/user-settings-page/PinCodeInput.svelte';
-  import { AppRoute } from '$lib/constants';
   import { handleError } from '$lib/utils/handle-error';
-  import { verifyPinCode } from '@immich/sdk';
+  import { unlockAuthSession } from '@immich/sdk';
   import { Icon } from '@immich/ui';
   import { mdiLockOpenVariantOutline, mdiLockOutline, mdiLockSmart } from '@mdi/js';
   import { t } from 'svelte-i18n';
@@ -23,17 +22,15 @@
   let hasPinCode = $derived(data.hasPinCode);
   let pinCode = $state('');
 
-  const onPinFilled = async (code: string, withDelay = false) => {
+  const handleUnlockSession = async (code: string) => {
     try {
-      await verifyPinCode({ pinCodeSetupDto: { pinCode: code } });
+      await unlockAuthSession({ sessionUnlockDto: { pinCode: code } });
 
       isVerified = true;
 
-      if (withDelay) {
-        await new Promise((resolve) => setTimeout(resolve, 1000));
-      }
+      await new Promise((resolve) => setTimeout(resolve, 1000));
 
-      void goto(data.continuePath ?? AppRoute.LOCKED);
+      await goto(data.continueUrl);
     } catch (error) {
       handleError(error, $t('wrong_pin_code'));
       isBadPinCode = true;
@@ -64,7 +61,7 @@
           bind:value={pinCode}
           tabindexStart={1}
           pinLength={6}
-          onFilled={(pinCode) => onPinFilled(pinCode, true)}
+          onFilled={handleUnlockSession}
         />
       </div>
     </div>
diff --git a/web/src/routes/auth/pin-prompt/+page.ts b/web/src/routes/auth/pin-prompt/+page.ts
index b0d248ebe6..89d59a3127 100644
--- a/web/src/routes/auth/pin-prompt/+page.ts
+++ b/web/src/routes/auth/pin-prompt/+page.ts
@@ -1,3 +1,4 @@
+import { AppRoute } from '$lib/constants';
 import { authenticate } from '$lib/utils/auth';
 import { getFormatter } from '$lib/utils/i18n';
 import { getAuthStatus } from '@immich/sdk';
@@ -8,8 +9,6 @@ export const load = (async ({ url }) => {
 
   const { pinCode } = await getAuthStatus();
 
-  const continuePath = url.searchParams.get('continue');
-
   const $t = await getFormatter();
 
   return {
@@ -17,6 +16,6 @@ export const load = (async ({ url }) => {
       title: $t('pin_verification'),
     },
     hasPinCode: !!pinCode,
-    continuePath,
+    continueUrl: url.searchParams.get('continue') || AppRoute.LOCKED,
   };
 }) satisfies PageLoad;