Zašto je sigurnost mobilnih aplikacija važnija nego ikad
Hajde da budemo iskreni — mobilne aplikacije danas imaju pristup gotovo svemu. Bankovni računi, zdravstveni kartoni, poslovni dokumenti, osobne poruke... Kad razmislite o tome, svaki sigurnosni propust može biti katastrofalan. I to ne samo za korisnika, nego i za vas koji ste tu aplikaciju napisali. Prema izvješćima iz 2025., više od 60% sigurnosnih proboja u mobilnim aplikacijama dolazi od neadekvatnog upravljanja autentifikacijom i pohranom osjetljivih podataka. Ta brojka me i dalje iznenađuje, ali istovremeno i ne čudi kad vidim kako se neki projekti rade.
U .NET MAUI ekosustavu zapravo imate solidne alate za izgradnju sigurnih aplikacija. Samo ih trebate ispravno koristiti — što je, naravno, lakše reći nego napraviti. SecureStorage za zaštićenu pohranu tokena, biometrijska autentifikacija za verifikaciju korisnika, i certificate pinning za zaštitu mrežne komunikacije — to su temelji na kojima gradite sigurnu mobilnu aplikaciju.
U ovom vodiču proći ćemo kroz svaki od tih aspekata s konkretnim primjerima koda, platformski specifičnim konfiguracijama i najboljim praksama koje možete odmah primijeniti u svojim projektima. Bez previše teorije, krenimo od temelja.
SecureStorage: Sigurna pohrana osjetljivih podataka
.NET MAUI dolazi s ugrađenim ISecureStorage API-jem za sigurno pohranjivanje malih količina osjetljivih podataka — tokena pristupa, osvježavanja tokena, API ključeva i sličnih tajni. Ono što mi se sviđa kod ovog API-ja je to što koristi platformski specifične mehanizme enkripcije. Vi pišete isti kod, a svaka platforma koristi najbolje alate koje ima na raspolaganju.
Kako SecureStorage radi ispod haube
Svaka platforma pristupa ovome malo drugačije:
- Android — koristi
EncryptedSharedPreferencesiz Android Security biblioteke. Ključevi i vrijednosti automatski se šifriraju pomoću AES-256 GCM algoritma. Zanimljiv detalj: enkripcija je nedeterministička, što znači da ista vrijednost pohranjena dva puta daje različiti šifrirani tekst - iOS — koristi Keychain, Appleov sigurnosni sustav za pohranu osjetljivih podataka. Podaci su zaštićeni hardverskom enkripcijom uređaja i dostupni samo vašoj aplikaciji
- Windows — koristi
DataProtectionProviderza šifriranje vrijednosti. Enkriptirani podaci pohranjeni su uApplicationData.Current.LocalSettings
Osnovne operacije s SecureStorage
API je namjerno jednostavan, i to cijenim. Pohrana, čitanje i brisanje — to je to:
// Pohrana tokena nakon uspješne prijave
await SecureStorage.Default.SetAsync("access_token", loginResult.AccessToken);
await SecureStorage.Default.SetAsync("refresh_token", loginResult.RefreshToken);
await SecureStorage.Default.SetAsync("token_expiry", loginResult.ExpiresAt.ToString("O"));
// Čitanje tokena
string? accessToken = await SecureStorage.Default.GetAsync("access_token");
string? refreshToken = await SecureStorage.Default.GetAsync("refresh_token");
// Brisanje jednog ključa
SecureStorage.Default.Remove("access_token");
// Brisanje svih pohranjenih podataka (npr. pri odjavi)
SecureStorage.Default.RemoveAll();
Jedna korisna stvar: metoda GetAsync vraća null ako ključ ne postoji. To vam omogućuje da pri pokretanju aplikacije jednostavno provjerite je li korisnik prijavljen, bez ikakvog try-catcha ili posebne logike.
Kreiranje servisa za upravljanje tokenima
Nemojte pozivati SecureStorage direktno posvuda po kodu. Ozbiljno, to je recept za probleme. Umjesto toga, napravite centralizirani servis. Lakše je za testiranje, lakše za promjenu implementacije, i (što je možda najvažnije) daje vam jedno mjesto gdje možete dodati logiku poput automatskog osvježavanja tokena:
public interface ITokenService
{
Task<string?> GetAccessTokenAsync();
Task<string?> GetRefreshTokenAsync();
Task SaveTokensAsync(string accessToken, string refreshToken, DateTime expiresAt);
Task ClearTokensAsync();
Task<bool> IsAuthenticatedAsync();
Task<bool> IsTokenExpiredAsync();
}
public class TokenService : ITokenService
{
private const string AccessTokenKey = "access_token";
private const string RefreshTokenKey = "refresh_token";
private const string TokenExpiryKey = "token_expiry";
private readonly ISecureStorage _secureStorage;
public TokenService(ISecureStorage secureStorage)
{
_secureStorage = secureStorage;
}
public async Task<string?> GetAccessTokenAsync()
=> await _secureStorage.GetAsync(AccessTokenKey);
public async Task<string?> GetRefreshTokenAsync()
=> await _secureStorage.GetAsync(RefreshTokenKey);
public async Task SaveTokensAsync(
string accessToken, string refreshToken, DateTime expiresAt)
{
await _secureStorage.SetAsync(AccessTokenKey, accessToken);
await _secureStorage.SetAsync(RefreshTokenKey, refreshToken);
await _secureStorage.SetAsync(TokenExpiryKey, expiresAt.ToString("O"));
}
public async Task ClearTokensAsync()
{
_secureStorage.Remove(AccessTokenKey);
_secureStorage.Remove(RefreshTokenKey);
_secureStorage.Remove(TokenExpiryKey);
}
public async Task<bool> IsAuthenticatedAsync()
{
var token = await GetAccessTokenAsync();
return !string.IsNullOrEmpty(token);
}
public async Task<bool> IsTokenExpiredAsync()
{
var expiryStr = await _secureStorage.GetAsync(TokenExpiryKey);
if (string.IsNullOrEmpty(expiryStr))
return true;
if (DateTime.TryParse(expiryStr, out var expiry))
return DateTime.UtcNow >= expiry;
return true;
}
}
Registracija u DI kontejneru je trivijalna:
builder.Services.AddSingleton<ISecureStorage>(SecureStorage.Default);
builder.Services.AddSingleton<ITokenService, TokenService>();
Primijetite da injektiramo ISecureStorage umjesto da koristimo statički SecureStorage.Default. Ovo možda izgleda kao sitnica, ali kad dođe vrijeme za pisanje unit testova, bit ćete zahvalni. Mogućnost zamjene implementacije u testovima je nešto što razdvaja testabilan kod od onog drugog.
Zamka na Androidu: Auto Backup
Ovo me ujelo na jednom projektu i potrošio sam dobar dio popodneva na debugging. Android 6.0+ automatski radi sigurnosnu kopiju podataka aplikacije, uključujući SharedPreferences. Problem? Šifrirani podaci se ne mogu dešifrirati nakon restauracije na novom uređaju jer su enkripcijski ključevi vezani za originalni uređaj.
.NET MAUI automatski detektira ovu situaciju i briše neupotrebljive ključeve, ali to znači da će korisnik morati ponoviti prijavu. Nije kraj svijeta, ali može zbuniti korisnike.
Ako želite potpuno izbjeći ovaj problem, isključite Auto Backup za SecureStorage dodavanjem u AndroidManifest.xml:
<application android:allowBackup="true"
android:fullBackupContent="@xml/backup_rules">
</application>
I kreirajte backup_rules.xml u Platforms/Android/Resources/xml/:
<?xml version="1.0" encoding="utf-8"?>
<full-backup-content>
<exclude domain="sharedpref"
path="__maui_secure_storage" />
</full-backup-content>
Biometrijska autentifikacija: Otisak prsta i Face ID
.NET MAUI trenutno nema ugrađenu podršku za biometrijsku autentifikaciju. Postoji otvoreni zahtjev na GitHubu za uvođenje BiometricAuthentication klase u Microsoft.Maui.Security prostor imena, ali dok se to ne dogodi (a tko zna kad će), oslanjamo se na NuGet pakete. Trenutno najbolji izbor je Oscore.Maui.Biometric (verzija 2.4.1) — aktivno održavan nasljednik napuštenog Plugin.Fingerprint paketa.
Instalacija i konfiguracija
Dodajte NuGet paket:
dotnet add package Oscore.Maui.Biometric
Registrirajte ga u MauiProgram.cs:
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.UseBiometricAuthentication();
return builder.Build();
}
Platformska konfiguracija
Android — dodajte dozvolu u Platforms/Android/AndroidManifest.xml:
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
iOS — dodajte opis za Face ID u Platforms/iOS/Info.plist:
<key>NSFaceIDUsageDescription</key>
<string>Aplikacija koristi Face ID za sigurnu prijavu</string>
Bez ovog opisa na iOS-u, aplikacija će se srušiti pri pokušaju korištenja Face ID-a. I kad kažem "srušiti", mislim na to doslovno — nema upozorenja, nema lijepog error messagea, samo pad aplikacije. Naučio sam to na teži način.
Implementacija biometrijskog servisa
Kao i sa SecureStorageom, napravite servis koji enkapsulira biometrijsku logiku. Ostatak aplikacije ne treba znati detalje implementacije:
public interface IBiometricService
{
Task<bool> IsBiometricAvailableAsync();
Task<BiometricResult> AuthenticateAsync(string reason);
}
public class BiometricService : IBiometricService
{
public async Task<bool> IsBiometricAvailableAsync()
{
var availability = await BiometricAuthentication
.Current.GetAvailabilityAsync();
return availability == BiometricAvailability.Available;
}
public async Task<BiometricResult> AuthenticateAsync(string reason)
{
var request = new AuthenticationRequest(
title: "Potvrda identiteta",
reason: reason);
request.AllowAlternativeAuthentication = true;
request.FallbackTitle = "Koristi PIN";
return await BiometricAuthentication
.Current.AuthenticateAsync(request);
}
}
Svojstvo AllowAlternativeAuthentication je tu iz dobrog razloga — omogućuje korisniku da koristi PIN ili lozinku uređaja ako biometrija ne uspije. Mokri prsti, maska na licu, loše osvjetljenje... ima puno razloga zašto biometrija može zakazati. Ovo je stvar pristupačnosti koliko i korisničkog iskustva.
Korištenje u ViewModelu
Evo kako to izgleda kad sve spojite u MVVM arhitekturi s CommunityToolkit.Mvvm. Ovaj dio je malo duži, ali vrijedi ga proći u cijelosti:
public partial class LoginViewModel : ObservableObject
{
private readonly IBiometricService _biometricService;
private readonly ITokenService _tokenService;
private readonly IApiService _apiService;
[ObservableProperty]
private bool _isBiometricAvailable;
[ObservableProperty]
private bool _isLoading;
[ObservableProperty]
private string? _errorMessage;
public LoginViewModel(
IBiometricService biometricService,
ITokenService tokenService,
IApiService apiService)
{
_biometricService = biometricService;
_tokenService = tokenService;
_apiService = apiService;
}
public async Task InitializeAsync()
{
IsBiometricAvailable = await _biometricService
.IsBiometricAvailableAsync();
}
[RelayCommand]
private async Task BiometricLoginAsync()
{
if (IsLoading) return;
try
{
IsLoading = true;
ErrorMessage = null;
var result = await _biometricService
.AuthenticateAsync("Prijavite se otiskom prsta ili Face ID-om");
if (!result.IsSuccessful)
{
ErrorMessage = "Biometrijska provjera nije uspjela. Pokušajte ponovno.";
return;
}
// Biometrija uspješna — provjeri postoji li pohranjen token
var refreshToken = await _tokenService.GetRefreshTokenAsync();
if (string.IsNullOrEmpty(refreshToken))
{
ErrorMessage = "Sesija je istekla. Prijavite se lozinkom.";
return;
}
// Osvježi access token koristeći refresh token
var tokenResponse = await _apiService.RefreshTokenAsync(refreshToken);
await _tokenService.SaveTokensAsync(
tokenResponse.AccessToken,
tokenResponse.RefreshToken,
tokenResponse.ExpiresAt);
await Shell.Current.GoToAsync("//main");
}
catch (Exception ex)
{
ErrorMessage = "Došlo je do greške. Pokušajte ponovno.";
}
finally
{
IsLoading = false;
}
}
}
Ovdje je ključna stvar koju mnogi razvijatelji pogrešno implementiraju: biometrija samo potvrđuje identitet korisnika na uređaju. Ona ne zamjenjuje pravi mehanizam autentifikacije prema serveru. Nakon što biometrija prođe, aplikacija koristi pohranjen refresh token za dobivanje novog access tokena. Biometrija je, u neku ruku, samo "čuvar" tog refresh tokena.
Automatsko osvježavanje tokena s DelegatingHandler
Svaki put kad pozivate zaštićeni API, morate priložiti validan access token. I da, možete tu logiku ručno pisati u svakom pozivu, ali... nemojte. Koristite DelegatingHandler — middleware za HTTP zahtjeve koji automatski dodaje token i osvježava ga kad istekne. Jednom napišete, i zaboravite na to.
public class AuthenticatedHttpClientHandler : DelegatingHandler
{
private readonly ITokenService _tokenService;
private readonly IApiService _apiService;
private readonly SemaphoreSlim _refreshLock = new(1, 1);
public AuthenticatedHttpClientHandler(
ITokenService tokenService,
IApiService apiService)
{
_tokenService = tokenService;
_apiService = apiService;
InnerHandler = new HttpClientHandler();
}
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
// Dodaj access token u zaglavlje
var token = await _tokenService.GetAccessTokenAsync();
if (!string.IsNullOrEmpty(token))
{
request.Headers.Authorization =
new System.Net.Http.Headers.AuthenticationHeaderValue(
"Bearer", token);
}
var response = await base.SendAsync(request, cancellationToken);
// Ako je token istekao (401), pokušaj osvježiti
if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized)
{
await _refreshLock.WaitAsync(cancellationToken);
try
{
// Provjeri je li drugi thread već osvježio token
var currentToken = await _tokenService.GetAccessTokenAsync();
if (currentToken == token)
{
var refreshToken = await _tokenService.GetRefreshTokenAsync();
if (!string.IsNullOrEmpty(refreshToken))
{
var newTokens = await _apiService
.RefreshTokenAsync(refreshToken);
await _tokenService.SaveTokensAsync(
newTokens.AccessToken,
newTokens.RefreshToken,
newTokens.ExpiresAt);
token = newTokens.AccessToken;
}
}
else
{
token = currentToken;
}
}
finally
{
_refreshLock.Release();
}
// Ponovi zahtjev s novim tokenom
var retryRequest = await CloneRequestAsync(request);
retryRequest.Headers.Authorization =
new System.Net.Http.Headers.AuthenticationHeaderValue(
"Bearer", token);
response = await base.SendAsync(retryRequest, cancellationToken);
}
return response;
}
private static async Task<HttpRequestMessage> CloneRequestAsync(
HttpRequestMessage request)
{
var clone = new HttpRequestMessage(request.Method, request.RequestUri);
if (request.Content != null)
{
var content = await request.Content.ReadAsByteArrayAsync();
clone.Content = new ByteArrayContent(content);
clone.Content.Headers.ContentType = request.Content.Headers.ContentType;
}
foreach (var header in request.Headers)
clone.Headers.TryAddWithoutValidation(header.Key, header.Value);
return clone;
}
}
Obratite pažnju na SemaphoreSlim. On osigurava da se token osvježava samo jednom, čak i kada više istovremenih zahtjeva dobije 401 odgovor. Bez tog mehanizma, lako možete završiti s desetak paralelnih zahtjeva za osvježavanje tokena — a to nije samo nepotrebno, nego može biti i ozbiljno problematično (pogotovo ako vaš backend invalidira refresh token nakon prve upotrebe).
Registracija HttpClienta s handlerom
builder.Services.AddTransient<AuthenticatedHttpClientHandler>();
builder.Services.AddHttpClient("AuthenticatedClient")
.AddHttpMessageHandler<AuthenticatedHttpClientHandler>();
Certificate pinning: Zaštita mrežne komunikacije
HTTPS štiti podatke u transportu, to svi znamo. Ali ima jednu slabu točku o kojoj se nedovoljno priča — oslanja se na sustav certifikacijskih autoriteta (CA). Ako napadač uspije ubaciti lažni CA certifikat na uređaj (recimo putem kompromitiranog Wi-Fi-ja na nekom kaficu), može presresti komunikaciju čak i preko HTTPS-a. Zvuči paranoično? Možda. Ali certificate pinning rješava upravo ovaj problem tako što vaša aplikacija prihvaća samo unaprijed definirane certifikate.
Implementacija u managed kodu
Najjednostavniji pristup za .NET MAUI koristi HttpClientHandler.ServerCertificateCustomValidationCallback:
public class CertificatePinningHandler : HttpClientHandler
{
// SHA-256 hash javnog ključa vašeg servera
private static readonly string[] PinnedPublicKeyHashes = new[]
{
"sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
// Backup pin za rotaciju certifikata
"sha256/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB="
};
public CertificatePinningHandler()
{
ServerCertificateCustomValidationCallback = ValidateCertificate;
}
private static bool ValidateCertificate(
HttpRequestMessage message,
System.Security.Cryptography.X509Certificates.X509Certificate2? cert,
System.Security.Cryptography.X509Certificates.X509Chain? chain,
System.Net.Security.SslPolicyErrors sslErrors)
{
#if DEBUG
// U razvoju dopusti sve certifikate
return true;
#else
if (cert == null) return false;
// Izračunaj SHA-256 hash javnog ključa
using var sha256 = System.Security.Cryptography.SHA256.Create();
var publicKeyBytes = cert.GetPublicKey();
var hashBytes = sha256.ComputeHash(publicKeyBytes);
var certHash = "sha256/" + Convert.ToBase64String(hashBytes);
return PinnedPublicKeyHashes.Contains(certHash);
#endif
}
}
Vidite da ima dva pina — primarni i rezervni. Ovo nije slučajno. Kad dođe vrijeme za rotaciju certifikata na serveru (a doći će, vjerujte mi), proces ide ovako: dodajte novi pin kao rezervni, objavite ažuriranje aplikacije, pričekajte da većina korisnika ažurira, i tek onda zamijenite certifikat na serveru. Ako preskočite ovaj korak, korisnici sa starom verzijom aplikacije jednostavno neće moći pristupiti serveru. A to nikome ne treba.
Android network_security_config pristup
Na Androidu postoji i alternativni, deklarativni pristup putem network_security_config.xml. Nekima je pregledniji:
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<domain-config>
<domain includeSubdomains="true">api.vasadomena.hr</domain>
<pin-set expiration="2027-01-01">
<pin digest="SHA-256">AAAAAAAAAAAAAAAAAAAAAAAAA=</pin>
<pin digest="SHA-256">BBBBBBBBBBBBBBBBBBBBBBBBB=</pin>
</pin-set>
</domain-config>
</network-security-config>
Referencirajte ga u AndroidManifest.xml:
<application android:networkSecurityConfig="@xml/network_security_config" />
Atribut expiration na pin-set elementu zaslužuje posebnu pažnju. On postavlja datum isteka pinninga — nakon tog datuma, pinning se ignorira i aplikacija prelazi na standardnu validaciju certifikata. To je zapravo sigurnosna mreža. Jer ako zaboravite ažurirati pinove (a ljudi zaboravljaju), barem nećete potpuno blokirati korisnike.
Kompletna sigurnosna arhitektura: Sve zajedno
Dobro, sada kad smo prošli kroz sve individualne komponente, pogledajmo kako sve to izgleda kad se složi u jednu koherentnu cjelinu. Evo registracije svih servisa u MauiProgram.cs:
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.UseBiometricAuthentication();
// Sigurnosni servisi
builder.Services.AddSingleton<ISecureStorage>(SecureStorage.Default);
builder.Services.AddSingleton<ITokenService, TokenService>();
builder.Services.AddSingleton<IBiometricService, BiometricService>();
// HTTP klijent s autentifikacijom i certificate pinningom
builder.Services.AddTransient<AuthenticatedHttpClientHandler>();
builder.Services.AddHttpClient("SecureApi", client =>
{
client.BaseAddress = new Uri("https://api.vasadomena.hr");
})
.ConfigurePrimaryHttpMessageHandler<CertificatePinningHandler>()
.AddHttpMessageHandler<AuthenticatedHttpClientHandler>();
// API servis
builder.Services.AddSingleton<IApiService, ApiService>();
// ViewModeli
builder.Services.AddTransient<LoginViewModel>();
builder.Services.AddTransient<LoginPage>();
return builder.Build();
}
Tok autentifikacije od prijave do API poziva
Cjelokupni sigurnosni tok, kad ga razložite korak po korak, izgleda ovako:
- Prva prijava — korisnik unosi korisničko ime i lozinku. API vraća access token i refresh token. Tokeni se pohranjuju u SecureStorage
- Sljedeće pokretanje aplikacije — aplikacija provjerava postoji li pohranjen refresh token. Ako postoji, prikazuje biometrijsku provjeru
- Biometrija uspješna — koristi refresh token za dobivanje novog access tokena. Preusmjerava korisnika na glavni ekran
- API pozivi —
AuthenticatedHttpClientHandlerautomatski dodaje token u zaglavlje svakog zahtjeva. Ako token istekne, automatski ga osvježava bez ikakve intervencije korisnika - Certificate pinning — svaki HTTPS zahtjev prolazi kroz validaciju certifikata. Samo zahtjevi prema serverima s poznatim certifikatima prolaze
- Odjava — brisanje svih tokena iz SecureStorage-a i preusmjeravanje na ekran prijave
Kad sve ovo radi zajedno, imate prilično robusnu sigurnosnu arhitekturu. Nije neprobojiva (ništa nije), ali pokriva sve ključne vektore napada.
Dodatne sigurnosne mjere
Sprečavanje snimanja zaslona
Za aplikacije koje prikazuju osjetljive podatke — financije, zdravstveni podaci i slično — poželjno je spriječiti snimanje zaslona. Na Androidu je to zapravo dosta jednostavno, dodajte ovo u MainActivity.cs:
protected override void OnCreate(Bundle? savedInstanceState)
{
base.OnCreate(savedInstanceState);
Window?.SetFlags(
Android.Views.WindowManagerFlags.Secure,
Android.Views.WindowManagerFlags.Secure);
}
Zastavica FLAG_SECURE sprječava snimke zaslona i screen recording, a kao bonus prikazuje prazan ekran u popisu nedavnih aplikacija. Dosta elegantno rješenje za nešto što bi inače bilo komplicirano.
Detekcija root/jailbreak uređaja
Rootani ili jailbreakani uređaji su sigurnosni rizik jer napadač (ili korisnik sam) može pristupiti datotekama aplikacije, uključujući ono što je pohranjeno u SecureStorage. .NET MAUI nema ugrađenu detekciju za ovo, ali možete napraviti osnovne provjere:
public static class DeviceSecurityCheck
{
#if ANDROID
public static bool IsDeviceRooted()
{
string[] rootPaths = new[]
{
"/system/app/Superuser.apk",
"/system/xbin/su",
"/system/bin/su",
"/sbin/su",
"/data/local/xbin/su",
"/data/local/bin/su"
};
return rootPaths.Any(System.IO.File.Exists);
}
#elif IOS
public static bool IsDeviceRooted()
{
string[] jailbreakPaths = new[]
{
"/Applications/Cydia.app",
"/Library/MobileSubstrate/MobileSubstrate.dylib",
"/bin/bash",
"/usr/sbin/sshd",
"/etc/apt",
"/private/var/lib/apt/"
};
return jailbreakPaths.Any(System.IO.File.Exists);
}
#else
public static bool IsDeviceRooted() => false;
#endif
}
Moram biti iskren — ovo nisu savršene provjere. Napredni korisnici ih mogu zaobići bez previše muke. Ali pokrivaju većinu slučajeva i, što je možda važnije, odvraćaju oportunističke napade. Ponekad je dovoljno podići letvicu samo malo da se većina prijetnji eliminira.
Zaštita od debugiranja u produkciji
Napadači ponekad pokušavaju priključiti debugger na vašu aplikaciju kako bi analizirali promet ili izvukli podatke iz memorije. Ovo im možete otežati:
#if !DEBUG
[assembly: System.Diagnostics.Debuggable(
System.Diagnostics.DebuggableAttribute.DebuggingModes.None)]
#endif
Nije neprobojna zaštita, ali je još jedan sloj koji napadač mora probiti.
Česta pitanja (FAQ)
Podržava li .NET MAUI biometrijsku autentifikaciju bez dodatnih paketa?
Nažalost, ne. .NET MAUI trenutno nema ugrađenu podršku za biometrijsku autentifikaciju. Trebat ćete koristiti vanjski NuGet paket poput Oscore.Maui.Biometric (koji bih preporučio) ili stariji Plugin.Fingerprint. Postoji otvoreni zahtjev na GitHub repozitoriju za dodavanje nativne podrške u budućim verzijama, ali za sad se oslanjamo na community pakete.
Je li SecureStorage dovoljno siguran za pohranu access tokena?
Za većinu slučajeva — da, apsolutno. SecureStorage koristi platformski specifičnu enkripciju: Keychain na iOS-u i EncryptedSharedPreferences (AES-256 GCM) na Androidu, što je ozbiljna zaštita. Međutim, na rootanim ili jailbreakanim uređajima ta sigurnost može biti kompromitirana. Ako gradite nešto visoko osjetljivo (bankarstvo, zdravstveni podaci), kombinirajte SecureStorage s biometrijskom autentifikacijom i detekcijom rootanih uređaja. Jedan sloj zaštite nikad nije dovoljan za takve aplikacije.
Kako implementirati certificate pinning u .NET MAUI bez biblioteka treće strane?
Koristite HttpClientHandler.ServerCertificateCustomValidationCallback za validaciju certifikata u managed kodu — primjer smo pokazali ranije u članku. Na Androidu možete alternativno koristiti network_security_config.xml za deklarativni pristup. Ključna stvar: uvijek uključite backup pin za rotaciju certifikata i datum isteka. Inače riskirate da zaključate vlastite korisnike iz aplikacije.
Trebam li implementirati i biometriju i lozinku za prijavu?
Da, svakako trebate obje opcije. Biometrija je praktičnija za svakodnevnu upotrebu, ali korisnik mora imati mogućnost prijave lozinkom. Razloga je više: biometrijski senzor može ne raditi, korisnik može biti na novom uređaju, ili mu je jednostavno istekao refresh token. Prva prijava uvijek zahtijeva lozinku, a biometrija se aktivira za sve naknadne prijave.
Kako sigurno osvježavati access token u pozadini?
Koristite DelegatingHandler koji presreće HTTP 401 odgovore i automatski osvježava token pomoću refresh tokena. Kritičan detalj je korištenje SemaphoreSlim za zaključavanje — bez toga riskirate višestruke istovremene zahtjeve za osvježavanje, što može stvoriti probleme (posebno ako vaš backend invalidira refresh token nakon jednokratne upotrebe). Ako osvježavanje ne uspije, preusmjerite korisnika na ekran prijave — nema smisla pokušavati dalje bez validnog tokena.