Anmelden

CheckOkay's plattformübergreifende und quelloffene Authentifizierungslösung

CheckOkay's plattformübergreifende und quelloffene Authentifizierungslösung
Technologie

1. August 2024 7 min lesen

Zuletzt geändert am 17. August 2024

In der Rubrik Technologie posten wir Artikel über aus unserer Sicht spannende Stolpersteine, die uns bei der Entwicklung von CheckOkay begegnen. Der erste Beitrag widmet sich einer plattformunabhängigen Authentifizierungslösung. Wir entwickeln CheckOkay als quelloffene Anwendung. Wir waren daher auf der Suche nach einer ebenfalls quelloffenen Authentifizierungslibrary die sowohl kompatibel mit React Native für unsere iOS und Android Anwendungen als auch kompatibel mit React für die Web Anwendung ist. Die einzigen Anbieter einer solchen Library die wir gefunden haben waren große Konzerne wie Firebase oder Clerk. Da diese Tools nicht quelloffen sind, verwenden wir nun folgende Architektur:

Component Diagram

Hier sind zwei Grundelemente entscheident:

  • Um Nutzer in der Web Anwendung zu authentifizieren nutzen wir NextAuth.js. Nutzer können sich über das OpenID Connect Protokoll über bestehende Anbieter auf unserer Web App anmelden
  • Die iOS und Android Anwendungen nutzen das OAuth Protokoll zur Authentifikation: Nutzer werden bei der Anmeldung auf die Web App umgeleitet und melden sich auf dieser an. Nach der Anmeldung werden sie in die App zurückgeleitet mit einem einmaligen Authorization Code. Diesen tauschen die mobilen Anwendungen dann gegen einen Access und Refresh Token ein und persistieren so die Authentifizierung.

Schließlich haben wir einen Provider entwickelt der auf beiden Plattformen (Expo bzw. React Native und Nextjs bzw. React) die Authentifizierungsfunktionalität unter gleichen Methodenaufrufen bereitstellt:

const AuthContext = React.createContext<CommonAuthContext | null>(null)
 
export function AuthProvider(props: React.PropsWithChildren) {
    const { data: session, status, update } = useSession()
 
    const authValue = {
        signIn: async () => {
            await signIn()
            return true
        },
        signOut: async () => {
            await signOut()
            return true
        },
        user: session?.user,
        isLoading: status === 'loading',
    }
 
    return (
        <AuthContext.Provider value={authValue}>
            {props.children}
        </AuthContext.Provider>
    )
}

Die mobilen Anwendungen müssen den Access Token regelmäßig über den Refresh Token aktualisieren. Diese Logik haben wir in die TRPC Kommunikation implementiert. Vor jedem TRPC Aufruf prüfen wir, ob ein Token Refresh notwendig ist und führen diesen falls nötig durch. Hierfür haben wir in den TRPC Client folgenden Link hinzugefügt, wobei tokenRefreshNeeded und fetchAccessToken Funktionen sind die evaluieren ob der Token aktualisiert werden muss bzw. die Aktualisierung durchführen:

export const tokenRefreshLink =
    <AppRouter extends AnyRouter>({
        tokenRefreshNeeded,
        fetchAccessToken,
    }: TokenRefreshProperties): TRPCLink<AppRouter> =>
    () => {
        return ({ next, op }) => {
            // each link needs to return an observable which propagates results
            return observable((observer) => {
                void queue.add(async () => {
                    const shouldRenew = tokenRefreshNeeded(op)
                    if (shouldRenew) {
                        // ok we need to refresh the token
                        await fetchAccessToken(op)
                    }
 
                    next(op).subscribe({
                        next(value) {
                            observer.next(value)
                        },
                        error(error) {
                            observer.error(error)
                        },
                        complete() {
                            observer.complete()
                        },
                    })
                })
            })
        }
    }
Den gesamten Code finden Sie in unserem offenen Quellcode auf Github.