What is a JWT & why is it important for mobile developers?

JWT og Autorisation for Mobile Udviklere

08/02/2023

Rating: 4.38 (7269 votes)

I den moderne mobilapps verden står udviklere over for en række komplekse udfordringer. Hvor vi tidligere har dykket ned i forskellige problemstillinger, vil denne artikel fokusere på et kritisk aspekt: autorisation. Baseret på vores erfaringer hos Just Eat, vil vi udforske håndteringen af JSON Web Tokens (JWT) i mobile miljøer og dele værdifulde indsigter om, hvordan du kan forbedre stabiliteten i din app og sikre en problemfri brugeroplevelse.

What is a JWT & why is it important for mobile developers?
In fact, a JWT has either to be JWS or JWE (JSON Web Encryption). RFC 7515, RFC 7516, and RFC 7519 describe the various fields and claims in detail. What is relevant for mobile developers is the following: JWT is composed of 3 parts dot-separated: Header, Payload, Signature. The Payload is the only relevant part.

Traditionelt set anvendes OAuth 2.0 i kombination med JWT (JSON Web Token) som standardpraksis for autorisation. Mens der er rigeligt med online diskussioner om nye netværksstakke og rammer som Combine, har vi observeret, at emnet om korrekt håndtering af JWT, især parsing, brug og – vigtigst af alt – opdatering, sjældent får den opmærksomhed, det fortjener. Vores mål er at belyse de faldgruber, vi selv stødte på, og give dig konkrete anbefalinger til at undgå almindelige fejl, så dine brugere praktisk talt altid kan forblive logget ind.

Indholdsfortegnelse

Hvad er en JWT?

JWT står for JSON Web Token og er en åben industristandard, der bruges til at repræsentere 'claims' eller udsagn, som overføres mellem to parter. Når en JWT er signeret, kaldes den en JWS (JSON Web Signature). En JWT skal faktisk enten være en JWS eller en JWE (JSON Web Encryption). RFC 7515, RFC 7516 og RFC 7519 beskriver de forskellige felter og claims i detaljer. For mobiludviklere er følgende punkter særligt relevante:

  • En JWT består af tre dele, adskilt af et punkt: Header, Payload og Signature.
  • Payload er den eneste del, der er direkte relevant for mobiludvikleren. Headeren identificerer, hvilken algoritme der bruges til at generere signaturen. Der er gode grunde til ikke at verificere signaturen på klientsiden, hvilket gør signaturdelen mindre relevant for mobilapps.
  • En JWT har en udløbsdato. Udløbne tokens skal fornys eller opdateres.
  • En JWT kan indeholde en vilkårlig mængde ekstra information, der er specifik for din tjeneste.
  • Det er almindelig praksis at gemme JWT'er i appens nøglekæde (keychain) for sikkerhed.

Her er et gyldigt og meget kort token-eksempel, venligst udlånt af jwt.io/, som vi anbefaler at bruge til nemt at afkode tokens til fejlfindingsformål. Det viser tre base64-kodede fragmenter, der er sammenkædet med et punkt:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0OjE1MTYyMzkwMjIsImV4cCI6MTU3Nzc1MDQwMH0.7hgBhNK_ZpiteB3GtLh07KJ486Vfe3WAdS-XoDksJCQ

Det eneste felt, der er relevant for denne diskussion, er exp (Expiration Time), som er en del af Payload (det andet fragment). Dette claim identificerer tidspunktet, hvorefter JWT'en ikke længere må accepteres. For at acceptere en JWT kræves det, at den aktuelle dato/tid skal være før udløbstiden, der er angivet i exp-claimet. Det er accepteret praksis for implementører at tillade en lille frist, normalt ikke mere end et par minutter, for at tage højde for forskelle i uret (clock skew) mellem klient og server.

Nogle API-kald kræver, at brugeren er logget ind (brugerautentificerede kald), mens andre ikke gør (ikke-brugerautentificerede kald). JWT kan bruges i begge tilfælde, hvilket skaber en sondring mellem Klient JWT og Bruger JWT, som vi vil henvise til senere.

Problemet med Token-Opdatering

Det mest betydningsfulde problem, vi tidligere har oplevet, var fornyelsen af tokenet. Dette synes at være noget, der tages for givet af mobilfællesskabet, men i virkeligheden fandt vi ud af, at det er en ret skrøbelig del af autentificeringsflowet. Hvis det ikke gøres korrekt, kan det nemt medføre, at dine kunder bliver logget ud, med den frustration, vi alle har oplevet som app-brugere.

Just Eat-appen foretager flere API-kald ved opstart: den henter ordrehistorik for at kontrollere igangværende ordrer, henter de mest opdaterede forbrugerdetaljer osv. Hvis tokenet er udløbet, når brugeren starter appen, kan en ubehagelig racebetingelse forårsage, at det samme opdateringstoken (refresh token) bruges to gange, hvilket får serveren til at svare med en 401 Unauthorized og efterfølgende logger brugeren ud af appen. Dette kan også ske under normal udførelse, når flere API-kald udføres meget tæt på hinanden, og tokenet udløber, inden disse kald er fuldført.

Situationen bliver endnu mere kompliceret, hvis klientens og serverens ure er mærkbart ude af synkronisering: Mens klienten måske tror, den er i besiddelse af et gyldigt token, er det allerede udløbet på serveren. Dette fører til uventede 401-svar og en dårlig brugeroplevelse.

Almindelig Dårlig Praksis

Vi har desværre sjældent fundet et firma (uanset størrelse) eller en uafhængig udvikler, der har implementeret en optimal token-opdateringsmekanisme. Den mest almindelige tilgang synes at være: opdater tokenet, når et API-kald fejler med 401 Unauthorized. Dette forårsager ikke kun et ekstra kald, som kunne undgås ved lokalt at kontrollere, om tokenet er udløbet, men det åbner også døren for den racebetingelse, der er illustreret ovenfor. At reagere på en 401 er at handle proaktivt, når problemet allerede er opstået, snarere end at forhindre det.

Undgå Racebetingelser ved Opdatering af Token 🚦

Den bedste måde at beskytte sig mod racebetingelser på er ved at bruge trådprimitiver, når asynkrone anmodninger om at hente et gyldigt token planlægges. Dette betyder, at alle kald vil blive reguleret via et filter, der vil tilbageholde efterfølgende kald, indtil et gyldigt token er hentet, enten fra lokal lagring eller, hvis en opdatering er nødvendig, fra den eksterne OAuth-server. Dette skaber en enkelt, kontrolleret strøm for token-håndtering.

For enkelhedens skyld antager vi, at kun brugerautentificerede API-anmodninger skal levere en JWT, som almindeligvis placeres i Authorization-headeren:

Authorization: Bearer <jwt-token>

Netværksklienten anmoder en AuthorizationValueProvider-instans om at levere en gyldig brugerautorisationsværdi (JWT'en), før den udfører en brugerautentificeret anmodning. Dette gøres via en asynkron metode, der bruger en seriel kø til at håndtere anmodningerne. Den afgørende del er logikken inden for den gensidige udelukkelse, som i vores løsning opnås ved brug af en seriel kø og en semafor.

Implementeringsprincipper

Forestil dig følgende flow:

  1. En API-anmodning skal udføres.
  2. Anmodningen sendes til en AuthorizationValueProvider.
  3. Provideren placerer anmodningen i en seriel kø for at sikre, at token-håndteringslogikken kun udføres én gang ad gangen.
  4. Inden for denne kø bruges en semafor til at opretholde gensidig udelukkelse, så kun én tråd kan få adgang til den kritiske sektion, hvor tokenets gyldighed kontrolleres og opdateres.
  5. Provideren kontrollerer, om det eksisterende token i den lokale lagring (keychain) stadig er gyldigt.
  6. Hvis tokenet er gyldigt, returneres det øjeblikkeligt.
  7. Hvis tokenet er udløbet, startes en asynkron anmodning om at opdatere tokenet fra OAuth-serveren. Alle andre anmodninger, der er i køen, venter på semaforen, indtil dette opdateringskald er fuldført.
  8. Når et nyt token er modtaget, lagres det lokalt, og semaforen frigives, hvilket tillader den næste anmodning i køen at fortsætte.
  9. Hvis opdateringen fejler, skal fejlhåndteringen være robust.

Semaforen initialiseres med en værdi på 1, hvilket betyder, at kun én tråd kan få adgang til den kritiske sektion ad gangen. Vi sørger for at kalde wait() i begyndelsen af udførelsen og kun kalde signal(), når vi har et resultat og derfor er klar til at forlade den kritiske sektion. Uden dette ville den næste anmodning til AuthorizationValueProvider blive poppet fra køen og udført, før den eksterne opdatering er afsluttet, hvilket igen ville føre til racebetingelser.

Håndtering af Fejl og Token-Udløb

Hvis tokenet, der findes i den lokale lagring, stadig er gyldigt, returnerer vi det blot. Ellers er det tid til at anmode om et nyt. I sidstnævnte tilfælde, hvis alt går godt, lagrer vi tokenet lokalt og tillader den næste anmodning at få adgang til metoden. I tilfælde af en fejl skal vi være forsigtige og kun slette tokenet, hvis fejlen er en legitim klientfejl (f.eks. en 4xx-statuskode). Dette inkluderer også brugen af et opdateringstoken, der ikke længere er gyldigt, hvilket kan ske, for eksempel hvis brugeren nulstiller adgangskoden på en anden platform/enhed.

Det er kritisk vigtigt ikke at slette tokenet fra den lokale lagring i tilfælde af andre fejl, såsom 5xx-serverfejl eller almindelige netværksfejl (f.eks. 'ikke forbundet til internettet'). Hvis tokenet slettes ved disse fejl, ville brugeren uventet blive logget ud, selvom problemet ikke var relateret til deres autentificering. En robust implementering skelner skarpt mellem autentificeringsfejl og midlertidige netværks- eller serverproblemer.

Det er også vigtigt at bemærke, at den samme AuthorizationValueProvider-instans skal bruges af alle kald: Brug af forskellige instanser ville betyde brug af forskellige køer, hvilket ville gøre hele løsningen ineffektiv. Vi fandt, at netværksklienten, vi udviklede internt, skulle omfavne JWT-opdateringslogikken i sin kerne, så alle API-kald, selv nye, der vil blive tilføjet i fremtiden, ville bruge det samme autentificeringsflow. Dette sikrer konsistens og reducerer udviklingsbyrden betydeligt.

Generelle Anbefalinger

Her er et par yderligere (mindre) forslag, som vi mener er værd at dele, da de kan spare dig for implementeringstid eller påvirke designet af din løsning.

Korrekt Parsing af Payload

Et andet problem – omend ret trivielt og ikke meget diskuteret – er parsing af JWT'en, som kan fejle i nogle tilfælde. I vores tilfælde var dette relateret til base64-kodningsfunktionen og 'justering' af base64-payloaden for at blive korrekt parset. I nogle implementeringer af base64 er 'padding'-tegnet (=) ikke nødvendigt for afkodning, da antallet af manglende bytes kan beregnes, men i Apples Foundation-implementering er det obligatorisk. Dette gav os en del hovedbrud, og et StackOverflow-svar hjalp os på rette vej.

Løsningen er – mere officielt – angivet i RFC 7515 - Appendix C. Essensen er, at base64-url-safe kodning (som JWT bruger) erstatter + med - og / med _, og fjerner padding. Når du afkoder med en standard base64-dekoder, der forventer padding og de standardtegn, skal du først konvertere tegnsættet tilbage og derefter tilføje padding, hvis nødvendigt. For eksempel, i Swift, kunne det se sådan ud:

func base64String(_ input: String) -> String {
var base64 = input
.replacingOccurrences(of: "-", with: "+")
.replacingOccurrences(of: "_", with: "/")
switch base64.count % 4 {
case 2:
base64 = base64.appending("==")
case 3:
base64 = base64.appending("=")
default:
break
}
return base64
}

Langt de fleste udviklere er afhængige af eksterne biblioteker for at lette parsing af tokenet. Selvom vi ofte implementerer vores løsninger fra bunden, uden at stole på et tredjepartsbibliotek, er vi af den opfattelse, at 'JSONWebToken' af Kyle Fuller er et meget godt bibliotek, der trofast implementerer JWT i overensstemmelse med RFC'en, inklusive den nødvendige base64-afkodningsfunktion.

Håndter Flere JWT'er for Forskellige App-Tilstande

Som tidligere nævnt, når JWT bruges som en autentificeringsmetode for ikke-brugerautentificerede kald, skal vi tage højde for mindst tre tilstande, vist i følgende enum:

TilstandBeskrivelse
.notAuthenticatedAppen er frisk installeret, ingen JWT er hentet.
.clientAuthenticatedEn gyldig Klient JWT er hentet og gemt lokalt. Denne bruges til generelle API-kald, der ikke kræver brugerlogin.
.userAuthenticatedBrugeren er logget ind, og en Bruger JWT er hentet og gemt lokalt. Denne bruges til bruger-specifikke API-kald.

Ved en frisk installation kan vi forvente at være i .notAuthenticated-tilstanden, men så snart det første API-kald skal udføres, skal en gyldig Klient JWT hentes og lagres lokalt (på dette stadium bruges andre autentificeringsmekanismer, sandsynligvis Basic Auth), hvilket flytter appen til .clientAuthenticated-tilstanden. Når brugeren har gennemført login- eller registreringsproceduren, hentes en Bruger JWT og lagres lokalt (men separat fra Klient JWT'en), hvilket fører til .userAuthenticated-tilstanden. Dette sikrer, at i tilfælde af et logout, forbliver appen med en (forhåbentlig stadig gyldig) Klient JWT.

I dette scenarie er næsten alle overgange mulige mellem disse tilstande. For eksempel kan en bruger logge ud fra .userAuthenticated og vende tilbage til .clientAuthenticated, eller appen kan starte i .clientAuthenticated, hvis en tidligere Klient JWT stadig er gyldig. En god implementering bør håndtere disse overgange flydende.

Et par anbefalinger her:

  • Hvis brugeren er logget ind, er det vigtigt at bruge Bruger JWT også til de ikke-brugerautentificerede kald, da serveren kan personalisere svaret (f.eks. listen over restauranter i Just Eat-appen baseret på brugerens præferencer eller tidligere ordrer). Dette giver en mere skræddersyet og relevant oplevelse for brugeren.
  • Gem både Klient JWT og Bruger JWT separat. Dette er afgørende for at sikre, at hvis brugeren logger ud, forbliver appen med Klient JWT'en klar til brug for at udføre ikke-brugerautentificerede anmodninger. Dette sparer et unødvendigt kald for at hente et nyt token og sikrer, at appen forbliver funktionel, selv når brugeren er logget ud.

Konklusion

I denne artikel har vi delt nogle af de erfaringer, vi har opnået ved at håndtere JWT på mobil, som ikke almindeligvis diskuteres inden for udviklingsfællesskabet. Det er vores håb, at disse indsigter kan hjælpe dig med at bygge mere robuste og brugervenlige mobilapplikationer.

Som en god praksis er det altid bedst at skjule kompleksitet og implementeringsdetaljer. At indbygge den beskrevne opdateringslogik i din API-klient er en fremragende måde at undgå, at udviklere skal håndtere kompleks logik for at levere autorisation. Det gør det muligt for alle API-kald, selv nye, at gennemgå den samme autentificeringsmekanisme. Forbrugere af en API-klient bør ikke have mulighed for at hente JWT'en, da det ikke er deres anliggende at bruge den eller pille ved den. Abstraktion er nøglen til at opretholde en ren arkitektur og minimere fejl.

Vi håber, at denne artikel hjælper med at øge bevidstheden om, hvordan man bedre håndterer brugen af JWT i mobile applikationer, især ved at sikre, at vi altid gør vores bedste for at undgå utilsigtede logouts for at give en bedre brugeroplevelse. En stabil og pålidelig login-oplevelse er grundlæggende for brugerfastholdelse og tilfredshed, og ved at følge disse anbefalinger kan du markant forbedre din apps kvalitet.

Ofte Stillede Spørgsmål (FAQ)

Hvad er en JWT, og hvorfor er den vigtig for mobiludviklere?

En JWT (JSON Web Token) er en sikker metode til at overføre information mellem parter. Den er vigtig for mobiludviklere, fordi den giver en standardiseret og sikker måde at håndtere brugerautentificering og autorisation på. Ved at bruge JWT kan apps verificere brugerens identitet og adgangsrettigheder uden at skulle sende følsomme loginoplysninger med hvert kald, hvilket forbedrer både sikkerhed og ydeevne.

Hvorfor er det så svært at håndtere token-opdatering korrekt på mobil?

Token-opdatering er kompleks på mobil på grund af potentielle racebetingelser, især når flere API-kald udføres samtidigt, og tokenet udløber. Hvis flere kald forsøger at opdatere det samme token, kan det føre til, at serveren afviser anmodninger og logger brugeren ud. Derudover kan forskelle i klient- og server-ure forårsage, at et token, der lokalt virker gyldigt, allerede er udløbet på serveren, hvilket skaber uventede 401-fejl.

Hvor skal jeg opbevare JWT'er i min mobilapp?

Det er almindelig praksis og stærkt anbefalet at opbevare JWT'er sikkert i appens nøglekæde (keychain). Nøglekæden er designet til at opbevare følsomme data sikkert og beskytter tokenet mod uautoriseret adgang fra andre apps eller systemkomponenter på enheden.

Skal jeg verificere JWT-signaturen på klientsiden?

Generelt anbefales det ikke at verificere JWT-signaturen på klientsiden i mobile apps. Signaturen er beregnet til at bekræfte tokenets integritet og ægthed over for serveren. Mobilappen skal behandle tokenet som en ugennemsigtig streng, der blot sendes til serveren. Serveren er ansvarlig for at verificere signaturen og sikre, at tokenet ikke er blevet manipuleret med.

Hvad er forskellen mellem Klient JWT og Bruger JWT?

Klient JWT bruges til ikke-brugerautentificerede kald, f.eks. til at hente generel information, der ikke kræver en specifik bruger at være logget ind. Bruger JWT derimod er knyttet til en specifik bruger og bruges til kald, der kræver, at brugeren er logget ind og autentificeret, såsom at hente brugerprofiloplysninger eller ordrehistorik. Det er vigtigt at håndtere og opbevare disse to typer tokens separat.

Hvorfor skal jeg undgå at slette tokenet ved netværksfejl?

Du bør kun slette tokenet, hvis fejlen indikerer et autentificeringsproblem (f.eks. en 401 Unauthorized grundet et ugyldigt token eller refresh token). Hvis tokenet slettes ved generelle netværksfejl (som 5xx-serverfejl eller manglende internetforbindelse), ville brugeren blive logget ud unødvendigt, selvom deres autentificering stadig er gyldig. Dette forringer brugeroplevelsen markant. Håndter fejl intelligent ved at skelne mellem server-, netværks- og autentificeringsfejl.

Hvis du vil læse andre artikler, der ligner JWT og Autorisation for Mobile Udviklere, kan du besøge kategorien Mobiludvikling.

Go up