🔐BAB 3 • Authentication Mobile
Cover1 / 25
🔐
🔐
🔐
🔐
🔐
🔐
🔒 MOBILE DEVELOPMENT • KELAS 11

BAB 3

AUTHENTICATION MOBILE

Bangun sistem login profesional seperti aplikasi nyata

📚 10 Topik
🎞 25 Slide
⚡ Flutter/Dart
🏆 Mini Project

Tekan → atau klik NEXT untuk melanjutkan

Apa yang Kita Pelajari?

🔐
Login API
📝
Register API
🚪
Logout API
🎫
JWT Token
Access Token
🔄
Refresh Token
🔒
Secure Storage
🕐
Session Mgmt
🤖
Auto Login
Auto Logout

Authentication = Kamu Siapa?

👤 ❌ TANPA KUNCI VS 👤 ✅ DENGAN KUNCI
Authentication = Membuktikan identitas
Sistem untuk memastikan "Siapa kamu?" sebelum memberikan akses. Seperti kunci pintu rumah — tanpa kunci yang benar, SIAPAPUN diblokir masuk. Ini yang menjaga keamanan aplikasi!
🔐 AUTHENTICATION
Siapa kamu?
Login email+password
Membuktikan identitas user
✅ AUTHORIZATION
✔️ Boleh ngapain?
Akses halaman admin/user
Memberikan permission

Login API - Masuk ke Sistem

📧 Email 🔑 Password 👤 SERVER Validate Email & Password ✅ BERHASIL access_token refresh_token user_data 1. User ketik data → 2. Server validasi → 3. Return token Status 200 = Sukses | Status 401 = Gagal
Flow Login:
  • 1. User Input: Ketik email & password di form
  • 2. POST Request: Kirim ke server `/auth/login` dengan method POST
  • 3. Server Validate: Server cek apakah email & password cocok di database
  • 4. Return Token: Jika cocok, server return access_token + refresh_token
  • 5. Simpan Token: App simpan token di Secure Storage untuk request berikutnya
class AuthService { Future<Map<String, dynamic>> login({ required String email, required String password, }) async { final response = await http.post( Uri.parse('https://api.example.com/auth/login'), headers: {'Content-Type': 'application/json'}, body: jsonEncode({ 'email': email, 'password': password, }), ); if (response.statusCode == 200) { // ✅ Login berhasil! return jsonDecode(response.body); } else if (response.statusCode == 401) { // ❌ Email atau password salah throw Exception('Kredensial tidak valid'); } else { throw Exception('Error: ${response.statusCode}'); } } }
⚠️ Jangan Lupa: Password HARUS dikirim over HTTPS (encrypted), bukan HTTP biasa. Selalu validasi email format sebelum kirim ke server!

Register API - Buat Akun Baru

📝 Name 📧 Email 🔑 Password 👤 POST SERVER 1. Hash password 2. Simpan ke DB 3. Create user ID 4. Return response ✅ AKUN BUAT Status: 201 (Created) Akun siap login! ✅ 201 = Akun berhasil dibuat ❌ 409 = Email sudah terdaftar | 422 = Validasi gagal
Perbedaan 201 vs 200:
201 Created: Resource BARU berhasil dibuat (register akun baru)
200 OK: Request berhasil, tapi bukan buat resource baru (login, ambil data)
Future<Map<String, dynamic>> register({ required String name, required String email, required String password, }) async { // Validasi sebelum kirim ke server if (password.length < 8) { throw Exception('Password minimal 8 karakter'); } final response = await http.post( Uri.parse('https://api.example.com/auth/register'), headers: {'Content-Type': 'application/json'}, body: jsonEncode({ 'name': name, 'email': email, 'password': password, }), ); if (response.statusCode == 201) { // ✅ Akun berhasil dibuat! return jsonDecode(response.body); } else if (response.statusCode == 409) { throw Exception('Email sudah terdaftar'); } else { throw Exception('Registrasi gagal'); } }
✅ Best Practice: Selalu validasi di Flutter SEBELUM kirim: email format valid, password panjang, nama tidak kosong. Ini membuat UX lebih baik!

Logout API - Keluar dengan Aman

👤 Token:xxxxx Klik Logout Kirim Token SERVER Invalidate token di DB ✅ OK Response APP clearAll() Hapus token lokal Navigate → Login ✅ Aman!
❌ LOGOUT SALAH: Hanya hapus token lokal tanpa kirim ke server. Masalahnya: Token masih bisa dipakai di server oleh orang lain yang mencuri token Anda!
✅ LOGOUT BENAR: (1) Kirim POST ke server dengan token → server invalidate token di DB (2) Tunggu response dari server (3) BARU hapus token lokal dan redirect ke login
Future<void> logout(String accessToken) async { try { // Langkah 1: Beri tahu server untuk invalidate token final response = await http.post( Uri.parse('https://api.example.com/auth/logout'), headers: { 'Authorization': 'Bearer $accessToken', 'Content-Type': 'application/json', }, ); if (response.statusCode != 200) { // Jika server response error, tetap lanjut ke langkah 2 debugPrint('Server logout gagal'); } } catch (e) { // Jika request gagal (network error), tetap lanjut debugPrint('Logout error: $e'); } finally { // Langkah 2: SELALU hapus token lokal, apapun hasilnya await _storage.clearAll(); // Langkah 3: Redirect ke login _navigateToLogin(); } }
🔑 Kunci Finally Block: Blok finally dijalankan SELALU, baik try berhasil atau catch. Ini memastikan token lokal selalu dihapus, jadi user TIDAK bisa masuk tanpa login ulang!

JWT - Tiket Digital Anti Palsu

HEADER Info Algoritma & Tipe Token PAYLOAD User ID, email, exp (kapan habis) SIGNATURE Stempel anti-palsu (Hanya server tahu) . .
JWT = JSON Web Token
Token yang terdiri dari 3 bagian, dipisahkan dengan TITIK (.). Format: HEADER.PAYLOAD.SIGNATURE
⚠️ PENTING: Setiap bagian JWT di-encode dengan Base64, BUKAN terenkripsi! Artinya, siapapun bisa decode dan baca isi PAYLOAD. Jadi JANGAN simpan password atau informasi sensitif di JWT!
✅ Aman Disimpan di JWT: User ID, email, role (admin/user), tanggal dibuat, tanggal expired
❌ Jangan Disimpan: Password, PIN, nomor rekening, API key
// Contoh JWT asli dari server: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 . eyJzdWIiOiJ1c2VyXzEyMyIsIm5hbWUiOiJCdWRpIFNhbnRvc28iLCJlbWFpbCI6ImJ1ZGlAZW1haWwuY29tIiwicm9sZSI6InN0dWRlbnQiLCJpYXQiOjE3MDAwMDAwMDAsImV4cCI6MTcwMDAwMzYwMH0 . SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c // Struktur: [Base64(Header)] . [Base64(Payload)] . [HmacSha256(Secret)]
BagianBisa Dibaca?Bisa Diubah?Fungsi
HEADER✅ Ya (Base64)❌ TidakTahu algoritma & tipe
PAYLOAD✅ Ya (Base64)❌ TidakData user (public info)
SIGNATURE✅ Ya (Base64)❌ Tidak*Validasi token asli dari server

Bedah Isi JWT - 3 Lapisan

⚙️
HEADER
👆 Klik untuk flip
{
"alg": "HS256",
"typ": "JWT"
}
👤
PAYLOAD
👆 Klik untuk flip
{
"sub": "user_123",
"name": "Budi",
"email": "budi@email.com",
"role": "student",
"iat": 1700000000,
"exp": 1700003600
}
🛡️
SIGNATURE
👆 Klik untuk flip
HMACSHA256(
base64url(header) +
"." +
base64url(payload),
"SECRET_KEY"
)
📍 iat = Issued At (Waktu dibuat)
Timestamp Unix ketika token dibuat. Server menggunakan ini untuk mendeteksi token yang terlalu lama atau digunakan ulang.
📍 exp = Expiration Time (Waktu habis)
Timestamp Unix kapan token tidak lagi valid. Jika sekarang > exp, token EXPIRED dan harus di-refresh!
✅ Cara Kerja Signature: Server punya SECRET_KEY yang hanya dia tahu. Signature dibuat dari HEADER + PAYLOAD + SECRET_KEY. Jika client mengubah PAYLOAD sedikit saja, signature tidak akan cocok, dan server akan menolak token tersebut!

Access Token - Tiket Masuk Sementara

⏱ Waktu Berlaku: PENDEK (15 menit – 2 jam)
Dibuat saat login, baru ketika user aktif melakukan request ke API.
📤 Penggunaan: Dikirim di SETIAP request ke server dalam header Authorization
Format: Authorization: Bearer eyJhbGc...
❌ TIDAK BISA: Diperbarui sendiri. Jika expired (kadaluarsa), harus gunakan Refresh Token untuk dapat token baru.
// Kirim access token di setiap request final response = await http.get( Uri.parse('https://api.example.com/user/profile'), headers: { 'Authorization': 'Bearer $accessToken', }, ); if (response.statusCode == 401) { // Token expired! Perlu refresh await refreshAccessToken(); }

Refresh Token - Memperpanjang Akses

Access Token Expired: 2 jam KEDALUWARSA! Refresh Token Valid: 30 hari 🔄 MASIH BERLAKU Kirim SERVER Validasi Refresh Token Generate token baru ✅ Kirim access_token baru
Refresh Token = Kunci Cadangan
Jika access token sudah expired tapi refresh token masih valid, maka user TIDAK perlu login ulang. Cukup kirim refresh token ke server, server akan membuat access token baru!
Access Token
⏱ Waktu: 15 min - 2 jam
🔑 Sering dipakai
❌ TIDAK bisa refresh sendiri
Refresh Token
⏱ Waktu: 7 - 30 hari
🔄 Jarang dipakai
✅ Bisa dipakai berkali-kali
// FLOW: Access token expired → gunakan refresh token if (response.statusCode == 401) { // Access token expired! Gunakan refresh token final refreshToken = await _storage.getRefreshToken(); final newResponse = await http.post( Uri.parse('$_baseUrl/auth/refresh'), body: jsonEncode({'refresh_token': refreshToken}), ); if (newResponse.statusCode == 200) { // ✅ Dapat token baru! final data = jsonDecode(newResponse.body); await _storage.saveAccessToken(data['access_token']); // Retry request asli return await _retryRequest(originalRequest); } else { // ❌ Refresh gagal = harus login ulang await _logout(); } }

Alur Lengkap: Access + Refresh Token

STEP 1 Request API dengan access_token Response OK? YA ✅ SELESAI Tampilkan data ke user TIDAK ❌ Ada Refresh Token? YA REFRESH Dapatkan access token baru Retry TIDAK LOGIN ULANG
⚠️ PENTING: Ini adalah flow OTOMATIS yang harus di-implement di interceptor HTTP request. User tidak perlu tahu — app handle sendiri!
✅ Best Practice: Buat HttpClient wrapper yang otomatis handle refresh token saat 401. Jadi di setiap screen, dev hanya perlu panggil API biasa saja!

Secure Storage - Brankas Digital

Laci Biasa SharedPreferences token_abc123 ❌ TIDAK AMAN Brankas Secure Storage ✅ AMAN (Encrypted) VS
FiturSharedPreferencesSecure Storage
Enkripsi❌ Plain text✅ AES-256
OS Protection❌ Visible✅ Keychain (iOS) / Keystore (Android)
Root/Jailbreak❌ Data bisa diambil✅ Protected bahkan dengan root
Cocok untukTheme, Language, Preference UI🔑 Token, Password, API Key, Secret
KecepatanSangat cepatCepat (tapi sedikit lebih lambat)
📍 Implementasi:
Tambah ke pubspec.yaml: flutter_secure_storage: ^9.0.0
// ✅ BENAR: Simpan token dengan Secure Storage final storage = FlutterSecureStorage(); // Simpan token await storage.write( key: 'access_token', value: token, ); // Baca token final token = await storage.read( key: 'access_token', ); // Hapus token await storage.delete(key: 'access_token'); // Hapus SEMUA await storage.deleteAll();

Session Management - Atur Sesi User

Sesi = Jangka waktu user aktif login
Start: User login berhasil → token disimpan
Active: User membuka app, melakukan request API
End: User logout → token dihapus
class SessionManager { final _storage = FlutterSecureStorage(); // 1. Apakah user punya sesi aktif? Future<bool> hasActiveSession() async { final token = await _storage.read( key: 'refresh_token', ); return token != null && token.isNotEmpty; } // 2. Mulai sesi setelah login berhasil Future<void> startSession({ required String accessToken, required String refreshToken, }) async { await _storage.write( key: 'access_token', value: accessToken); await _storage.write( key: 'refresh_token', value: refreshToken); await _storage.write( key: 'login_time', value: DateTime.now().toIso8601String()); } // 3. Akhiri sesi saat logout Future<void> endSession() async { await _storage.deleteAll(); } }

Auto Login - Masuk Otomatis

📱 Buka App Splash Screen Loading... Ada Refresh Token? YA ✅ HOME TIDAK ❌ LOGIN Input email & password ✅ TANPA ketik password lagi! Refresh token dipakai untuk validasi otomatis
class SplashScreen extends StatefulWidget { @override State<SplashScreen> createState() => _SplashScreenState(); } class _SplashScreenState extends State<SplashScreen> { @override void initState() { super.initState(); _checkAutoLogin(); // ← Panggil saat widget dibuat } Future<void> _checkAutoLogin() async { // Tampilkan splash 2 detik await Future.delayed( const Duration(seconds: 2), ); // Cek: apakah ada sesi aktif? final hasSession = await SessionManager() .hasActiveSession(); if (!mounted) return; // Navigasi berdasarkan hasil Navigator.pushReplacementNamed( context, hasSession ? '/home' : '/login', ); } }
✅ User Experience: User buka app → langsung HOME (kalau sudah login) atau LOGIN (kalau belum). Tidak perlu manual input token!

Auto Logout - Keluar Otomatis

2 JENIS AUTO LOGOUT:
1️⃣ Token Expired: Server response 401 → app otomatis logout
2️⃣ Inactivity Timeout: User tidak ada input 30 menit → app otomatis logout
// JENIS 1: Token Expired (401 dari server) if (response.statusCode == 401) { // Token expired atau invalid! await _logout(); // Redirect ke login dengan pesan _showSnackBar('Sesi habis, silakan login kembali'); } // JENIS 2: Inactivity Timeout (30 menit tidak aktif) class InactivityTimer { static Timer? _timer; static const _timeout = Duration(minutes: 30); // Panggil ini setiap user input (tap, scroll, ketik) static void reset(BuildContext context) { _timer?.cancel(); _timer = Timer(_timeout, () { // 30 menit berlalu tanpa input! _logout(context); }); } static Future<void> _logout( BuildContext context) async { await SessionManager().endSession(); Navigator.pushNamedAndRemoveUntil( context, '/login', (_) => false, ); } }
💡 Implementasi: Di setiap screen utama, tambahkan GestureDetector untuk trigger InactivityTimer.reset() saat user interact. Contoh:
GestureDetector( onTap: () => InactivityTimer.reset(context), child: YourWidget(), )

Authentication Flow Lengkap

Alur Lengkap:
1. Buka App → Splash Screen
2. Cek Secure Storage: Ada Refresh Token?
3. YA → Langsung ke HOME | TIDAK → Ke LOGIN
4. Login → POST email+password → Server
5. Return access_token + refresh_token
6. Simpan ke Secure Storage
7. Go to HOME
8. Setiap request: Header + access_token
9. Token expired (401)? → Refresh → Retry request
10. Logout → POST ke server → Hapus token lokal → Go to LOGIN

Mini Project - Overview

📁 Struktur Folder:
lib/
├── main.dart
├── models/
│ └── user_model.dart
├── services/
│ ├── auth_service.dart
│ └── token_storage.dart
├── screens/
│ ├── splash_screen.dart
│ ├── login_screen.dart
│ ├── register_screen.dart
│ └── home_screen.dart
└── utils/
    └── session_manager.dart
✅ Fitur Checklist:
☑️ Register (3 field: name, email, password)
☑️ Login (2 field: email, password)
☑️ Auto Login (cek storage saat buka app)
☑️ Logout (hapus token + navigate)
☑️ Secure Storage (flutter_secure_storage)
☑️ Loading States (show spinner)
☑️ Error Handling (show snackbar)

Setup Project & Dependencies

# pubspec.yaml name: authentication_app description: Aplikasi autentikasi mobile profesional environment: sdk: '>=3.0.0 <4.0.0' dependencies: flutter: sdk: flutter http: ^1.2.0 flutter_secure_storage: ^9.0.0 dev_dependencies: flutter_test: sdk: flutter flutter_lints: ^3.0.0 flutter: uses-material-design: true
📝 Setup Steps:
1. flutter create authentication_app
2. Edit pubspec.yaml (copy atas)
3. flutter pub get
4. Buat folder struktur di /lib
5. Mulai coding dari splash_screen.dart

Login Screen - Implementasi Lengkap

⚠️ Flow: User input email+password → Validasi → POST ke server → Dapat token → Simpan ke storage → Navigate ke HOME
class _LoginScreenState extends State<LoginScreen> { final _emailCtrl = TextEditingController(); final _passwordCtrl = TextEditingController(); final _formKey = GlobalKey<FormState>(); bool _isLoading = false; @override void dispose() { _emailCtrl.dispose(); _passwordCtrl.dispose(); super.dispose(); } Future<void> _handleLogin() async { // 1️⃣ Validasi form if (!_formKey.currentState!.validate()) { return; } setState(() => _isLoading = true); try { // 2️⃣ Panggil auth service final result = await AuthService().login( email: _emailCtrl.text.trim(), password: _passwordCtrl.text, ); // 3️⃣ Simpan token ke secure storage await SessionManager().startSession( accessToken: result['access_token'], refreshToken: result['refresh_token'], ); if (!mounted) return; // 4️⃣ Navigasi ke HOME Navigator.pushReplacementNamed(context, '/home'); } catch (e) { // Tampilkan error dengan snackbar ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Login gagal: $e'), backgroundColor: Colors.red, ), ); } finally { setState(() => _isLoading = false); } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Login')), body: Form( key: _formKey, child: Column( children: [ TextFormField( controller: _emailCtrl, decoration: InputDecoration( labelText: 'Email', ), validator: (val) { if (val?.isEmpty ?? true) { return 'Email tidak boleh kosong'; } return null; }, ), SizedBox(height: 16), TextFormField( controller: _passwordCtrl, obscureText: true, decoration: InputDecoration( labelText: 'Password', ), validator: (val) { if (val?.length ?? 0 < 8) { return 'Password min 8 karakter'; } return null; }, ), SizedBox(height: 24), _isLoading ? const CircularProgressIndicator() : ElevatedButton( onPressed: _handleLogin, child: const Text('Login'), ), ], ), ), ); } }
✅ Key Points:
  • trim(): Hapus spasi di awal/akhir email
  • validate(): Cek form input sebelum submit
  • try-catch-finally: Handle error dengan baik
  • !mounted: Cek sebelum navigasi agar tidak error jika widget sudah dilepas

Register Screen - Implementasi Lengkap

⚠️ Flow: Input name+email+password → Validasi lengkap → POST ke server → Akun terbuat → Navigate ke LOGIN
class _RegisterScreenState extends State<RegisterScreen> { final _nameCtrl = TextEditingController(); final _emailCtrl = TextEditingController(); final _passwordCtrl = TextEditingController(); final _confirmCtrl = TextEditingController(); bool _isLoading = false; Future<void> _handleRegister() async { // Validasi email format final emailRegex = RegExp( r'^[a-zA-Z0-9.]+@[a-zA-Z0-9]+\.[a-zA-Z]+', ); if (!emailRegex.hasMatch(_emailCtrl.text)) { _showError('Email format tidak valid'); return; } // Validasi password match if (_passwordCtrl.text != _confirmCtrl.text) { _showError('Password tidak cocok'); return; } setState(() => _isLoading = true); try { await AuthService().register( name: _nameCtrl.text, email: _emailCtrl.text.trim(), password: _passwordCtrl.text, ); if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: const Text('Akun berhasil dibuat! Silakan login.'), backgroundColor: Colors.green, ), ); Navigator.pushReplacementNamed( context, '/login', ); } catch (e) { _showError('Register gagal: $e'); } finally { setState(() => _isLoading = false); } } void _showError(String msg) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(msg)), ); } }
✅ Validasi yang Penting:
  • Email format: Gunakan regex untuk cek @domain
  • Password length: Min 8 karakter
  • Password match: Konfirmasi password harus sama
  • Name tidak kosong: Wajib diisi

Home Screen - Tampil Data & Logout

class _HomeScreenState extends State<HomeScreen> { final _session = SessionManager(); String? _userName; bool _isLoggingOut = false; @override void initState() { super.initState(); _loadUserData(); } Future<void> _loadUserData() async { // Ambil data user dari storage atau API final name = await _session.getCurrentUserName(); setState(() => _userName = name); } Future<void> _handleLogout() async { setState(() => _isLoggingOut = true); try { // 1️⃣ Ambil token final token = await FlutterSecureStorage() .read(key: 'access_token'); // 2️⃣ Kirim logout ke server if (token != null) { await AuthService().logout(token); } // 3️⃣ Hapus token lokal await _session.endSession(); if (!mounted) return; // 4️⃣ Navigate ke login Navigator.pushReplacementNamed( context, '/login', ); } catch (e) { // Even if error, still force logout local await _session.endSession(); } finally { setState(() => _isLoggingOut = false); } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Home'), actions: [ IconButton( onPressed: _handleLogout, icon: const Icon(Icons.logout), ), ], ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon( Icons.account_circle, size: 80, ), Text( 'Selamat datang, $_userName!', style: const TextStyle( fontSize: 20, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 24), _isLoggingOut ? const CircularProgressIndicator() : ElevatedButton( onPressed: _handleLogout, child: const Text('Logout'), ), ], ), ), ); } }
✅ Important Pattern: Saat logout, SELALU hapus token lokal di finally block, bahkan jika server logout gagal (network error). Ini memastikan user dipaksa login ulang!

Demo Interaktif - Alur Lengkap

👇 Klik setiap STEP untuk lihat detail request dan response:
📝
STEP 1
Register Akun Baru
🔐
STEP 2
Login & Dapat Token
🏠
STEP 3
Request API dengan Token
🔄
STEP 4
Token Expired → Refresh
🔁
STEP 5
Retry Request dengan Token Baru
🚪
STEP 6
Logout & Clear Token
👆 Klik salah satu STEP di atas untuk melihat detail...

Latihan - Quiz Interaktif 🎯

Latihan - Coding Challenge ✏️

Soal 1: Nama header untuk kirim token
Soal 2: Method simpan token ke Secure Storage
Soal 3: Status HTTP untuk akses token expired

Tugas BAB 3 🚀

Buat Aplikasi Flutter Bernama: auth_tugas_[namaKamu]
API yang digunakan: https://reqres.in/api (API publik gratis untuk testing)
✅ ENDPOINT API:
• Register: POST /api/register (body: email, password)
• Login: POST /api/login (body: email, password)
• Logout: Implementasi manual (clear storage + navigate)
📝 KRITERIA PENILAIAN:
⭐ Register + form validation = 20 poin
⭐ Login + token tersimpan secure = 25 poin
⭐ Splash screen + auto login = 20 poin
⭐ Logout + clear token + navigate = 15 poin
⭐ UI rapi + loading states + error handling = 20 poin
TOTAL = 100 POIN
📤 FORMAT PENGUMPULAN:
📁 Folder: auth_tugas_[nama_kamu]/
📹 Video demo screen recording (max 5 menit)
📝 Laporan singkat (jelaskan flow authentication)
📤 Kumpulkan ke: Google Classroom / Folder Guru
📅 Deadline: 1 minggu dari hari ini