Laravel Sanctum mobile app authentication in Swift

Laravel authentication

Using Laravel out of the box with the Laravel Jetstream starter kit, you'll end with Laravel Sanctum. It's a powerful package to create and manage API tokens from your own app. No more OAuth! It's main strength comes from same site usage (due to fancy seamless auth), but in this article we'll discuss how we can login over API routes that will use Sanctum.

Login controller for Sanctum

The client will make a request with the following payload:

  • Email
  • Password
  • Device name

Then we will run our business logic for login:

  1. Make sure the User exists
  2. Confirm password hash match
  3. Create a sanctum token and return both the user data and token string

App/Http/Controllers/UserController.php

1class UserController extends Controller
2{
3 public function login(LoginRequest $request)
4 {
5 $email = $request->validated()->get('email');
6 $password = $request->validated()->get('password');
7 $deviceName = $request->validated()->get('device_name');
8 
9 $user = User::where('email', $email)->first();
10 
11 if (! $user || ! Hash::check($password, $user->password)) {
12 throw ValidationException::withMessages([
13 'email' => ['The provided credentials are incorrect.'],
14 ]);
15 }
16 
17 $token = $user->createToken($deviceName)->plainTextToken;
18 
19 return [
20 'user' => $user,
21 'token' => $token,
22 ];
23 }
24}

routes/api.php

1Route::post('/sanctum/login', [UserController::class, 'login']);

Login on client in Swift

Data models

We'll create two models, one for the User and one for the combo of User and token.

1struct User: Codable, Hashable, Equatable {
2 let id: Int
3 let name: String
4 let email: String
5}
6 
7struct UserWithToken: Codable, Hashable, Equatable {
8 let user: User
9 let token: String
10}

API client login

Then we'll create a method in our APIClient to log in, sending an HTTP POST request with email, password, and device_name. It'll return the parsed UserWithToken struct, if successful.

1class APIClient {
2 func login(email: String, password: String) async throws -> UserWithToken {
3 let parameters = await ["email": email, "password": password, "device_name": UIDevice.current.name]
4 
5 var httpHeaders = headers ?? HTTPHeaders()
6 httpHeaders.add(.accept("application/json"))
7 
8 let request = session.request("myapp.test/sanctum/login", method: .post, parameters: parameters, headers: httpHeaders)
9 
10 let response = await request
11 .serializingDecodable(UserWithToken.self, decoder: decoder)
12 .response
13 
14 switch response.result {
15 case .success(let model):
16 return model
17 case .failure(let error):
18 throw APIError.AFError(error: error)
19 }
20 }
21}

Calling login and saving result in Keychain

Then at the call site, we can login and save the token to our Keychain. This is a secure spot to save it on device, so it's not easily read by others. We'll mark it with .accessibleAfterFirstUnlock for added security.

1do {
2 let userWithToken = try await apiClient.login(email: email, password: password)
3 let keychain = Keychain() // Using https://github.com/evgenyneu/keychain-swift here
4 keychain.set(token, forKey: "loginToken", withAccess: .accessibleAfterFirstUnlock)
5} catch {
6 // Show alert
7}

Determining if logged in

Tada! Now on app startup, we can check for existence of this "loginToken" and if it exists, attach it to our outgoing HTTP requests as a Bearer token and we are good to go! Remember: just because there is a token saved, does not mean the token is still valid. Try out the token and see what happens, the server knows best :)