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:
- Password
- Device name
Then we will run our business logic for login:
- Make sure the User exists
- Confirm password hash match
- 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: String10}
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 request11 .serializingDecodable(UserWithToken.self, decoder: decoder)12 .response13 14 switch response.result {15 case .success(let model):16 return model17 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 here4 keychain.set(token, forKey: "loginToken", withAccess: .accessibleAfterFirstUnlock)5} catch {6 // Show alert7}
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 :)