8. Google login met custom backend
8. Google login met custom backend
In deze les bespreken we hoe je Google authenticatie kan gebruiken in je mobiele app in combinatie met een custom backend. Het uitgewerkte voorbeeld bevat ook een voorbeeld over inloggen met email/password.
Backend
Volg de instructies in mobile les 6 om Google authentication op te zetten via Firebase. Je hoeft google-services.json niet in je project te plaatsen, je moet enkel het web client ID opslaan zoals beschreven in de les. Dit client ID is zowel nodig in de backend en mobiele applicatie.
Info
Backend
DATABASE_URL=postgresql://postgres:postgresPassword@localhost:5432/postgres?schema=public
JWT_SECRET=KittiesAreCatsButEvenMoreAdorableThanYouCanImagine
PRIVATE_KEY=LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUpRZ0lCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQ1N3d2dna29BZ0VBQW9JQ0FRQ05HQmE2d2xYK3BmRHkKZWdJRjhDWGF5a3RmVzR6OTY1Rm55SElNZTRhdUVIOXlTZzBRdkxlRVYza1RyV1p3dk1HTGxlbHZWYUdHVERneApJTlJuSzBYVTc0cDlSWWZkQVo1bFhXTU5VbGtkL0lSYzNqR05sbHg3ZjVydStORFlPNmRrZnh0WjNrUkcyLy9nCkw5RGZUZUpxNm5wSzJ0c3NQRGEyYmQ4WnI2UzZWL2dPcjRSQW0vdzdmWmlYaG1rMTMwRTRhQUlDRFlWdXRYYW8KdUp2MHJtK2QrMjJUSEJzYUh1VzJRbEtuWnVEYk81VG5jbmRDcjFMWlVVR2ZkazVaN3BSdDlTNmh6bUo4cXA0bQpyc2VUN3I5bG9tYndQTmJBV3hMcUhHTWw3VmtZWE5zSmpaWkw5cU5RdEttZjBZTkhRYmZUV2t5OEk0SEtibGc3CjJ6c3plMTB4NDBQUHlpN3IwcmUzdWdkRVlsYSt2U1NQd1BFN0V2R3FaYU1pdjBNRnJ1V3JTNlBvNHo3elRPemEKYWlPcDJnSmszMm0xazArc2dZbDV0Q2xoQWVHSHUyOG5yWjhtUjZNU1JEQ001YlRTN1prRGpRazNMY1JTUm5veApoRXF4VGV3Tm5OUThKbGJXRkU1RGxkdmNOUERjTEIrRW5WUzBSQWNqMmh2YXByUHNlUWRETVJJdm1WR1lKb0ltCklISFFGYk9ZL25BdlNHZzlPNW1QNVp1M1Z2RXc2cGZCKy9zSkpFWGRjcjJFYkJTK0s1M1BPbzdPVFpaaXRiTnMKSjRRQU5FSTVwY2xPRWJBR2JMem5nYzVUVEhHL3pNdzhCZUdyb3czL2RHd1liNkxlalJaaCsrZ3RBOXRORHA4dgpQek0yQ2pqNXNxbFVma2hIdEl1cVBhOElPT1VlcFFJREFRQUJBb0lDQUFOOHJwOGdaUWNYQlFNa1UyVjVrdFJVCnZ2bnVwWnJtNzZBakdzV3RORVJvMUpKUk96TTdtRjYxUCszbDNrdjNsa2lRa1d1cDR6Z3J4cXFNS25ibDRaWTkKdjFMRXFneWk4b0ZSQm5Bc0piMStGUklyc3p6UjRRSkF3cGxsa0tZdmNuRWpTM1lOMUxlTm0xK2JXSVd0NmNxNApkYzhUL3AxTktONDBXbGprZDdJTzVGRkJaZFIwODBFMlZ1MmFLZWpsMWgvbzJ2RnpDMyt0RFFqLzJ3T3JYNVE1CkVRem9OTkZFV05HaE9jcTR3SHdhbHlxZVk0QmQ2Umo0ZGFKdStaVlpuS0lZUTZsR1IybHhqQXdSUy9Sc1R0SnoKdHNJc05zQ0NFdWwvUytob3dXRnlENlhIR0NNZ1I2UUNGS0s1YlkycC9BZjRzQTBDc2FQOGJsd0ZKaVZRUnhyVApQUWQ1dzhoNkdiYXk2OGV2djJFUWRiOTFqU1pNMzJ5WjNYSG13ODlYam9JOVZFbzYrbmNRVEFJbXRoWFNwLy9HCnBaZzVyOUd2aHJhb1JlZ0FkZG4rNGMvZ2ZpK3BQL1BlT0FMOGtITXhoOS80bk1kZk5vYkxpeXFZdmhKVjd0K3UKVEZnTjdXdGhIS21yUERpVWFiWlE3TjhqZDY0NjMyQ0VVSlZHeUZuMnBYQ2lUZkpIK3phYmc1K1ZQVVJQdUN5LwovalpqY3padGRsUmJySktWZnZXVWRYY2tOaVBtWmw3cDdwWnU2aktuMnIxNE9CdUk4eExEMDdoUm9jOTRQUTJFCk1SOEFxUTN6NFBuRmM2Q3M2OUt4YzdCaFFuWldLdFhZa0t0UG0yWk15WFYzNkVhbVZGZ1BqR3lKd3AvOHFxRHIKd2tkSjhiZ0pJTnVnSWw5Ukw3R1JBb0lCQVFDOUg1T0dGR0ZBR0E4UFlyQ1V1WGwvWkJReUVWMUVrL0pjZk5mdApVWXhzNW84Sjl6bEY4ckkwZ1ZyenM4b2l1UDB2ejliZFdjRlN4VVVVNXMySjE2UTZWdGRFNUpsd0ZndWVZS2t5CjFVOWN4dDN2OUFtaXRsVUlmRzVYSVNrZDdENncrUkJlTHdpMGEyekhwUUxWTUVZWUpqbmlxK3hrc3UzN1FGL28KeU9aSGZCK2ErQVVta25tRG51Y1p4RktIQzloblJLNnNRdVJuTjlwaDFMRFl3V0hmWGNZZXNlSmFOZDJWRUw0cQovVnpVdWJTV0c0TkpMc0hRM3RlZjFiYkJvNHZrZXpnaTNsZnRMUGJNL21nZ0ZLMVExWW1kQkF1L1FnY05zK1FYCk5wZ25DOVp5aXFrMVQzMW1Da1dIWTNoVVBlTEhhaWN5azJlNmxPbm4yUzNFeVUxWkFvSUJBUUMrL0tnUFRlc0YKRm52aWFGdU1JMVI0c0RNM2lFL3E1Qld2TmFrUTdRd01PLzVNbjM2dm1lcFVhKzdPL1NjbVdRZ09WOTc3ZjFIQQpwbjUza0FyU0d1c0Z0NE0wOU1DOXdSb1h4YTJsNkNxSmRlZ0ZvS1d1YVZ2anU1UXZnYWFJZWZNL0hYTldZR1o1CitXRnZXaE8wMVlJTDhsUFRyUnlnMlY5Z2Rpc1NnUTY2azJSOStIQjZRVWRrNE5RVHJYdUJOUUtOTDlZRExsd08KMGkxUnIyODlnUlY4VXAvWldtUzFZTHMwODdyNEJKKzNEVDNoQ1RyekxQVGlPWkhGaWRydHZZV3ViVzI4R3pqYQpmSG5VWUMrZVpsVDNONHFhblV3VnZ3b1dZTm9SbU9pSVVGVThEZGkrZ1RFRlhKM0NnSFFuWmR0aHlZKzZhQVExCjhrVVdCNEY1eC9ZdEFvSUJBRVBiQW5sRzhxV21mRERPL2dBK2srTXJLenZUa3gwNTMrbGpPYTRDWmxua1Ywb0MKRStDTzlYVjFQQWJZYVJ3UU4zZGlJUHVHYXFDaCt1bEc3bFFZdE9uaG9wY3JWZ29yUHJ2eWZvS29takwxZjBmSwp6WEk5N1VDQU5LNjZUU0JaVlJ6MkgvTksrTnFTK0pLaEYrVVp6dXgzT3FtamVjdWZqMWRvZjREc0pBTmQrNUVFCmtBU0k1V1JlTFJqSG9ieW1lVnlEL2cvOGFDcklsV2dvZ2RNamYxUS9Od2hvVm9oN2J5Yyt2Sng4RDBTYjVMaysKNGRudUZzZzBURFVpM3RHWE5tZ2g5Y0E4K2xzMG1OQndMemZqYzFQZmFlS1dRd2k2VGdHU203SGdlckwzd2xlUwpmS2VOLzdZTzFXZUFQdm9xd2lVK1p4WDBZWEZHWUs0NDgvejVKUEVDZ2dFQUQ4NTVXa3JvbUpNenpaazFlbk95CkdncjdNaTFsNzlyUXNKK1FUb1pQNlBOT0tLbEtvdDNxTnZKMzRVbXlZOG9ha0pWVkx4dHFlTGRPNERaYnJ1ZnIKYTUyMGpqMXpka0QxRitLVWJKYTZib1lEbmZPZHdzR1ZpVk9OQUNHSzF1REE4UWhPODJjbkZCRS9yeWVWV2ZJUgp3VXRkQkxmZkZyRG00K1RqeTdSQ3M4NWZFczN0QVRGRjUyTjBLZXpCdS8vWEpqZy9UaVFZR21IcXZrZjJ2UEJpCkJ0Z1B3cjFvZUZwNUkvaklFSjdSV1NVUGFnQ0prSGE0RGNFVi8zTlpXelBFVEp0aDZaK1hKUDRJVnp0ZXRZMWkKbXROTjlWM3ZYaDFoaUZpYnM3a2tCYVFnYWNmN21FaTQ5ZWlrWmVTYnVHY0ZzU0l0Y3hBSXlTNHo2WTdWZUc3RQpRUUtDQVFFQXUzY2R3dnk5djRwQnN3YWRHdGcvWkI3SUtVaGJma01TU29mTFV5dkorT044VzM0REhLNSt0S2JUCnBCeUJKVy9abE5RUXdjN3ZWUzMwRDV2NTdsaVAvclRMems4NUJtdjhrdlJCVWhaZGFUV2Q2QXJJbUp0Z2kwNmsKcm91RGs4cmp6UHdLQnFGOGYyZFZ1ZWxaODM2WTFWazhuOXUyNHFOZjdJU2dSZ0dqWFVCbUQzMGdTdVk5cWFpagpUeS8wcDEyNWhQcmxmZW9NdFc0cW53WUxXcFFNRmgzVS85bnI5VEx4c250NlQwaGhEWXFUNjhJcEp5Zk0xSEpkCkVVbVlDVFR1Y1gra1AxNUlaMG11YzNvNFdrRjdZSE9xWUR5SUZ1cml0eGVBdnN5Zzc2MXB2NnhLQmdCeGdHclAKRVU5ek5zTnhmYzY2dURBWUppQURCUE1sZ3NTYTVBPT0KLS0tLS1FTkQgUFJJVkFURSBLRVktLS0tLQo=
PUBLIC_KEY=LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQ0lqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FnOEFNSUlDQ2dLQ0FnRUFqUmdXdXNKVi9xWHc4bm9DQmZBbAoyc3BMWDF1TS9ldVJaOGh5REh1R3JoQi9ja29ORUx5M2hGZDVFNjFtY0x6Qmk1WHBiMVdoaGt3NE1TRFVaeXRGCjFPK0tmVVdIM1FHZVpWMWpEVkpaSGZ5RVhONHhqWlpjZTMrYTd2alEyRHVuWkg4YldkNUVSdHYvNEMvUTMwM2kKYXVwNlN0cmJMRHcydG0zZkdhK2t1bGY0RHErRVFKdjhPMzJZbDRacE5kOUJPR2dDQWcyRmJyVjJxTGliOUs1dgpuZnR0a3h3YkdoN2x0a0pTcDJiZzJ6dVU1M0ozUXE5UzJWRkJuM1pPV2U2VWJmVXVvYzVpZktxZUpxN0hrKzYvClphSm04RHpXd0ZzUzZoeGpKZTFaR0Z6YkNZMldTL2FqVUxTcG45R0RSMEczMDFwTXZDT0J5bTVZTzlzN00zdGQKTWVORHo4b3U2OUszdDdvSFJHSld2cjBrajhEeE94THhxbVdqSXI5REJhN2xxMHVqNk9NKzgwenMybW9qcWRvQwpaTjlwdFpOUHJJR0plYlFwWVFIaGg3dHZKNjJmSmtlakVrUXdqT1cwMHUyWkE0MEpOeTNFVWtaNk1ZUktzVTNzCkRaelVQQ1pXMWhST1E1WGIzRFR3M0N3ZmhKMVV0RVFISTlvYjJxYXo3SGtIUXpFU0w1bFJtQ2FDSmlCeDBCV3oKbVA1d0wwaG9QVHVaaitXYnQxYnhNT3FYd2Z2N0NTUkYzWEs5aEd3VXZpdWR6enFPemsyV1lyV3piQ2VFQURSQwpPYVhKVGhHd0JteTg1NEhPVTB4eHY4ek1QQVhocTZNTi8zUnNHRytpM28wV1lmdm9MUVBiVFE2Zkx6OHpOZ280CitiS3BWSDVJUjdTTHFqMnZDRGpsSHFVQ0F3RUFBUT09Ci0tLS0tRU5EIFBVQkxJQyBLRVktLS0tLQo=
# GEBRUIK JE EIGEN ID
GOOGLE_WEB_CLIENT_ID=922020756960-6cvvhbg8t4hecpe292e3ig27ae8jrnaf.apps.googleusercontent.com:::
Database aanpassen
Aangezien een gebruiker kan inloggen met Google, moet de wachtwoord-kolom optioneel worden, een gebruiker die via Google inlog is niet verplicht om een wachtwoord in te stellen. Google biedt de mogelijkheid om het email-adres dat gekoppeld is aan de account te wijzigen, daarom mogen we een gebruiker die via Google inlogt niet identificeren aan de hand van het email-adres. In de plaats daarvan gebruiken we het Google id.
Info
Backend
model User {
id String @id @unique @default(dbgenerated("gen_random_uuid()")) @db.Uuid
email String @unique @db.VarChar(255)
password String?
username String
googleId String?
sessions Session[]
Contact Contact[]
Meeting Meeting[]
Tag Tag[]
}:::
Notitie
Deze aanpassing genereert verschillende linting fouten omdat het wachtwoord nu optioneel is. Het oplossen van deze fouten laten we aan de geïnteresseerde lezer.
Google id token verifiëren
Als de gebruiker inlogt via Google, ontvangt de mobiele applicatie een token. Deze id token kan geverifieerd worden op de backend en ingewisseld worden voor een token die specifiek is voor jouw applicatie. Hiervoor gebruiken we een nieuwe API route.
Om de token te verifiëren moet je de google-auth-library installeren.
We beginnen met de verificatie library te initialiseren. Vervolgens lezen we de Google token uit de headers en verifiëren we deze via het client id dat we hebben opgeslagen in de .env file. Daarna kunnen we de payload van de token uitlezen, i.e. de data die de gebruiker beschrijft.
Info
Backend
import {OAuth2Client, TokenPayload} from 'google-auth-library'
const client = new OAuth2Client()
export const GET = wrapper(async (request): Promise<NextResponse> => {
const googleToken = request.headers.get('authorization')?.split(' ')[1]
if (!googleToken) {
return new NextResponse(JSON.stringify({error: 'Unauthorized'}), {status: 401})
}
const ticket = await client.verifyIdToken({
idToken: googleToken,
audience: process.env.GOOGLE_WEB_CLIENT_ID,
})
if (!ticket) {
return new NextResponse(JSON.stringify({error: 'Unauthorized'}), {status: 401})
}
const payload = ticket.getPayload() as TokenPayload
return new NextResponse(JSON.stringify({token: ''}), {status: 200})
}, false):::
Natuurlijk moeten we ook controleren of de gebruiker al bestaat in de database, en indien niet, een nieuwe gebruiker aanmaken. Hiervoor schrijven we enkel nieuwe DAL-functies. Tenslotte maken we een JWT aan en geven we deze terug zoals besproken in backend frameworks les 4.
Info
Backend
export const GET = wrapper(async (request): Promise<NextResponse> => {
const googleToken = request.headers.get('authorization')?.split(' ')[1]
if (!googleToken) {
return new NextResponse(JSON.stringify({error: 'Unauthorized'}), {status: 401})
}
const ticket = await client.verifyIdToken({
idToken: googleToken,
audience: process.env.GOOGLE_WEB_CLIENT_ID,
})
if (!ticket) {
return new NextResponse(JSON.stringify({error: 'Unauthorized'}), {status: 401})
}
const payload = ticket.getPayload() as TokenPayload
let user = await DAL.getUserByGoogleId(payload.sub)
if (!user) {
user = await DAL.createSocialUser({
email: payload.email!,
username: payload.name ?? payload.email!.split('@')[0],
googleId: payload.sub,
})
}
const token = createJwtToken(user);
return new NextResponse(JSON.stringify({token}), {status: 200})
}, false)export async function createSocialUser(data: Prisma.UserCreateInput): Promise<User> {
return prismaClient.user.create({
data: {
...data,
password: null,
},
})
}
export async function getUserByGoogleId(googleId: string): Promise<User | null> {
return prismaClient.user.findFirst({where: {googleId}})
}:::
Tips
Je kan er ook voor kiezen om de accounts van Google en je eigen systeem te mergen. Maar dan moet je nog steeds het Google id bewaren omdat het email-adres kan wijzigen.
Mobiele applicatie
Om in te loggen via Google zijn de firebase libraries die in mobile development les 6 besproken zijn niet nodig. Enkel de react-native-google-signin library moet geïnstalleerd worden. Vergeet niet om het web client id toe te voegen.
Info
Mobile
# GEBRUIK JE EIGEN ID
EXPO_PUBLIC_WEB_CLIENT_ID=922020756960-6cvvhbg8t4hecpe292e3ig27ae8jrnaf.apps.googleusercontent.co:::
De token moet bewaard blijven als de applicatie herstart. Om dit veilig te doen maakt het voorbeeld gebruik van expo-secure-store.
Tenslotte schrijven we een functie waarmee de gebruiker kan inloggen via Google en waarmee de Google id token uitgewisseld wordt voor een token dat specifiek is voor onze applicatie. Natuurlijk gebeurt dit via de API route die hierboven geschreven is.
Naast de hoofdmethode worden er ook enkele hulpmethodes geschreven om de applicatie-specifieke JWT te bewaren en te decoderen zodat d gebruikersinformatie gelezen kan worden.
Info
Mobile
GoogleSignin.configure({webClientId: process.env.EXPO_PUBLIC_WEB_CLIENT_ID})
async function signInWithGoogle(): Promise<User | null> {
// Check if your device supports Google Play
await GoogleSignin.hasPlayServices({showPlayServicesUpdateDialog: true})
// Get the users ID token
const signInResult = await GoogleSignin.signIn()
// Retrieve the ID Token
const idToken = signInResult.data?.idToken
if (!idToken) {
throw new Error('No ID token found')
}
// Retrieve a JWT which can be authenticated on the backend.
// TODO Change This IP to the domain where you host your backend.
const response = await fetch('http://10.0.2.2:3000/api/auth/google', {
headers: {
Authorization: `Bearer ${idToken}`,
},
})
if (!response.ok) return null
const {token} = await ((await response.json()) as Promise<{token: string}>)
await SecureStore.setItemAsync('token', token)
return getUserFromToken(token)
}
async function getCurrentUser(): Promise<User | null> {
const token = await SecureStore.getItemAsync('token')
if (!token) return null
return getUserFromToken(token)
}
function getUserFromToken(token: string): User {
const payload = token.split('.')[1]
const decoded = atob(payload)
return JSON.parse(decoded) as User
}
export function getToken(): Promise<string | null> {
return SecureStore.getItemAsync('token')
}:::