{"course":{"slug":"building-apps-with-ai","title":"Building Apps with AI","url":"https://addaly.com/learn/building-apps-with-ai","level":"beginner","track":"build"},"lesson":{"slug":"sign-in-without-writing-it-yourself","title":"Sign-in, without writing it yourself","url":"https://addaly.com/learn/building-apps-with-ai/sign-in-without-writing-it-yourself","position":52,"of":87,"minutes":9,"answer":"Authentication is the one feature where the model's from-scratch answer is dangerous even when it works.","keyIdea":"Authentication is the one feature where the model's from-scratch answer is dangerous even when it works, so use a library or a hosted provider and learn the six-step OAuth dance well enough to debug the redirect error."},"body":{"format":"markdown","text":"## The feature you must not build from scratch\n\nAsk a model for \"user login\" and it will produce something that works: a users table with a password column, a form, a comparison. Older training data will hash the password with MD5 or SHA-1, or not at all. Newer data will use bcrypt and still get the surrounding parts wrong: the session, the reset flow, the timing of the comparison, the lockout after failures.\n\nAuthentication is the one part of an app where \"works\" and \"safe\" are furthest apart, because the failure is invisible: a badly hashed password table looks identical to a well hashed one until it is stolen. The rule is absolute for this course. Do not write it. Use a library or a hosted provider, and spend your attention on wiring it correctly.\n\n## The options\n\n**A library in your app.** Auth.js (formerly NextAuth), better-auth and Lucia's guide for hand-wired sessions are the common ones in the JavaScript world. They store users and sessions in your own database, cost nothing, and give you sign-in with Google, GitHub, email links and passwords with the hashing done properly. The trade: you run it, and you read its documentation.\n\n**A hosted provider.** Clerk, Auth0, Firebase Auth and Supabase Auth keep the users on their side and hand your app a verified identity. Generous free tiers (Clerk's covers ten thousand monthly active users at the time of writing) and a polished sign-in screen in ten minutes. The trade: your users live in somebody else's system, and leaving later means migrating them.\n\nEither is fine. Pick by which your framework's documentation shows first, and put the choice in the specification so the model does not pick a third.\n\n## What \"sign in with Google\" actually does\n\nThe flow has six steps, and knowing them is what lets you debug it.\n\n1. Your app sends the browser to Google with your **client id** and a **callback address**: the page on your site to come back to.\n2. Google shows its own consent screen. Your app never sees the password.\n3. Google sends the browser back to your callback address with a short-lived **code**.\n4. Your server exchanges the code, together with your **client secret**, for a token. This step is server to server, which is why the secret never reaches the browser.\n5. Your server asks Google who the token belongs to and gets an email and a name.\n6. Your app finds or creates a user with that email and starts a session.\n\nStep 1 is where the deployed site breaks. Google only sends users back to callback addresses you registered in advance, and the list you made on day one contains `http://localhost:3000/api/auth/callback/google` and nothing else. The error is `redirect_uri_mismatch`, it is not a code bug, and the fix is to add the production address to the list in the Google Cloud console. Every provider has the same check and the same error under a different name.\n\nThe client secret goes in environment variables, on the server side, without the public prefix. Step 4 is why: it is the proof that the request came from your server and not from a copy of your JavaScript.\n\n## Passwords, if you must\n\nSome audiences will not sign in with Google, and email-plus-password remains the most familiar form. The library hashes with bcrypt or argon2 (slow by design, so that a stolen table takes years to crack rather than minutes) and you will never see the plaintext. What remains yours: a minimum length (eight is the floor; do not add complexity rules, they push people towards `Password1!`), a rate limit on the sign-in route so that guessing is slow, and a reset flow that sends a one-time link rather than the password.\n\n## Magic links and codes\n\nAn email containing a link or a six-digit code, valid for ten minutes, is a password the user never has to remember. It moves the security to the inbox, which is where a password reset would have sent it anyway. It needs an email-sending service (Resend's free tier of three thousand a month is enough for a small app) and it needs the link to expire and to work once.\n\n## The parts you still own\n\nThe library authenticates. Your code decides what a signed-in person may do, which is the next lesson's subject and the security block's whole concern. And your code decides which identity the rest of the app refers to. Store your own `users` row and have bookings reference that, not the provider's id, so that switching providers later is a migration of one table rather than of every foreign key in the system.\n\n## Testing it\n\nMake a second Google account, or use the provider's test users. Sign up fresh, sign out, sign in again, reset a password, sign in from a private window while already signed in elsewhere. Every one of those is a path a user takes in the first week and none of them is the path the agent tested, because the agent only ever signed in once."},"check":{"question":"'Sign in with Google' works on the laptop and fails on the deployed site with 'redirect_uri_mismatch'. What has happened?","options":[{"text":"Google has rejected the app because it is not yet verified.","correct":false,"tempting":"Unverified apps show a warning screen to users; they do not produce a redirect mismatch."},{"text":"The client secret was not copied to the host's environment variables, so the deployed server cannot prove its identity to Google when it asks for the token.","correct":false,"tempting":"A missing secret fails later, at the token exchange, with an error about the client, not about the redirect."},{"text":"The callback address the deployed app sends is not on the list registered with Google, which still only contains the localhost one.","correct":true},{"text":"The browser is blocking third-party cookies.","correct":false,"tempting":"Cookie blocking breaks the session after sign-in; the redirect check happens on Google's side before any cookie is involved."}]},"prev":{"slug":"seed-data-and-the-empty-state","title":"Seed data, and the empty state nobody tested","url":"https://addaly.com/learn/building-apps-with-ai/seed-data-and-the-empty-state"},"next":{"slug":"sessions-cookies-and-who-you-are","title":"Sessions, cookies, and who the server thinks you are","url":"https://addaly.com/learn/building-apps-with-ai/sessions-cookies-and-who-you-are"},"authorship":"Founder and series editor: Dr Mustafa Mun. Written by AI models to a written standard. Exam keys checked blind by a second model: 3,665 of 3,666 agreed. Academic review invited.","review":{"status":"Draft, open for academic review"},"cite":"Addaly (2026) 'Sign-in, without writing it yourself', in Building Apps with AI. Edited by M. Mun. Addaly. Available at: https://addaly.com/learn/building-apps-with-ai/sign-in-without-writing-it-yourself (Accessed: 13 October 2026).","licence":{"name":"CC BY-NC-SA 4.0","url":"https://creativecommons.org/licenses/by-nc-sa/4.0/"},"reader":{"schema":1,"title":"Sign-in, without writing it yourself","courseTitle":"Building Apps with AI","module":{"index":6,"slug":"the-parts-that-persist","title":"The parts that persist: data, accounts and files"},"answer":null,"keyIdea":"Authentication is the one feature where the model’s from-scratch answer is dangerous even when it works, so use a library or a hosted provider and learn the six-step OAuth dance well enough to debug the redirect error.","hero":{"kind":"plate","image":null,"alt":null,"caption":null},"blocks":[{"t":"h2","id":"the-feature-you-must-not-build-from-scratch","runs":[{"text":"The feature you must not build from scratch"}]},{"t":"p","runs":[{"text":"Ask a model for “user login” and it will produce something that works: a users table with a password column, a form, a comparison. Older training data will hash the password with MD5 or SHA-1, or not at all. Newer data will use bcrypt and still get the surrounding parts wrong: the session, the reset flow, the timing of the comparison, the lockout after failures."}]},{"t":"p","runs":[{"text":"Authentication is the one part of an app where “works” and “safe” are furthest apart, because the failure is invisible: a badly hashed password table looks identical to a well hashed one until it is stolen. The rule is absolute for this course. Do not write it. Use a library or a hosted provider, and spend your attention on wiring it correctly."}]},{"t":"h2","id":"the-options","runs":[{"text":"The options"}]},{"t":"p","runs":[{"text":"A library in your app.","b":true},{"text":" Auth.js (formerly NextAuth), better-auth and Lucia’s guide for hand-wired sessions are the common ones in the JavaScript world. They store users and sessions in your own database, cost nothing, and give you sign-in with Google, GitHub, email links and passwords with the hashing done properly. The trade: you run it, and you read its documentation."}]},{"t":"p","runs":[{"text":"A hosted provider.","b":true},{"text":" Clerk, Auth0, Firebase Auth and Supabase Auth keep the users on their side and hand your app a verified identity. Generous free tiers (Clerk’s covers ten thousand monthly active users at the time of writing) and a polished sign-in screen in ten minutes. The trade: your users live in somebody else’s system, and leaving later means migrating them."}]},{"t":"p","runs":[{"text":"Either is fine. Pick by which your framework’s documentation shows first, and put the choice in the specification so the model does not pick a third."}]},{"t":"h2","id":"what-sign-in-with-google-actually-does","runs":[{"text":"What “sign in with Google” actually does"}]},{"t":"p","runs":[{"text":"The flow has six steps, and knowing them is what lets you debug it."}]},{"t":"ol","start":1,"items":[[{"text":"Your app sends the browser to Google with your "},{"text":"client id","b":true},{"text":" and a "},{"text":"callback address","b":true},{"text":": the page on your site to come back to."}],[{"text":"Google shows its own consent screen. Your app never sees the password."}],[{"text":"Google sends the browser back to your callback address with a short-lived "},{"text":"code","b":true},{"text":"."}],[{"text":"Your server exchanges the code, together with your "},{"text":"client secret","b":true},{"text":", for a token. This step is server to server, which is why the secret never reaches the browser."}],[{"text":"Your server asks Google who the token belongs to and gets an email and a name."}],[{"text":"Your app finds or creates a user with that email and starts a session."}]]},{"t":"p","runs":[{"text":"Step 1 is where the deployed site breaks. Google only sends users back to callback addresses you registered in advance, and the list you made on day one contains "},{"text":"http://localhost:3000/api/auth/callback/google","code":true},{"text":" and nothing else. The error is "},{"text":"redirect_uri_mismatch","code":true},{"text":", it is not a code bug, and the fix is to add the production address to the list in the Google Cloud console. Every provider has the same check and the same error under a different name."}]},{"t":"p","runs":[{"text":"The client secret goes in environment variables, on the server side, without the public prefix. Step 4 is why: it is the proof that the request came from your server and not from a copy of your JavaScript."}]},{"t":"h2","id":"passwords-if-you-must","runs":[{"text":"Passwords, if you must"}]},{"t":"p","runs":[{"text":"Some audiences will not sign in with Google, and email-plus-password remains the most familiar form. The library hashes with bcrypt or argon2 (slow by design, so that a stolen table takes years to crack rather than minutes) and you will never see the plaintext. What remains yours: a minimum length (eight is the floor; do not add complexity rules, they push people towards "},{"text":"Password1!","code":true},{"text":"), a rate limit on the sign-in route so that guessing is slow, and a reset flow that sends a one-time link rather than the password."}]},{"t":"h2","id":"magic-links-and-codes","runs":[{"text":"Magic links and codes"}]},{"t":"p","runs":[{"text":"An email containing a link or a six-digit code, valid for ten minutes, is a password the user never has to remember. It moves the security to the inbox, which is where a password reset would have sent it anyway. It needs an email-sending service (Resend’s free tier of three thousand a month is enough for a small app) and it needs the link to expire and to work once."}]},{"t":"h2","id":"the-parts-you-still-own","runs":[{"text":"The parts you still own"}]},{"t":"p","runs":[{"text":"The library authenticates. Your code decides what a signed-in person may do, which is the next lesson’s subject and the security block’s whole concern. And your code decides which identity the rest of the app refers to. Store your own "},{"text":"users","code":true},{"text":" row and have bookings reference that, not the provider’s id, so that switching providers later is a migration of one table rather than of every foreign key in the system."}]},{"t":"h2","id":"testing-it","runs":[{"text":"Testing it"}]},{"t":"p","runs":[{"text":"Make a second Google account, or use the provider’s test users. Sign up fresh, sign out, sign in again, reset a password, sign in from a private window while already signed in elsewhere. Every one of those is a path a user takes in the first week and none of them is the path the agent tested, because the agent only ever signed in once."}]}],"check":{"question":[{"text":"‘Sign in with Google’ works on the laptop and fails on the deployed site with ‘redirect_uri_mismatch’. What has happened?"}],"options":[{"text":[{"text":"Google has rejected the app because it is not yet verified."}],"correct":false,"tempting":[{"text":"Unverified apps show a warning screen to users; they do not produce a redirect mismatch."}]},{"text":[{"text":"The client secret was not copied to the host’s environment variables, so the deployed server cannot prove its identity to Google when it asks for the token."}],"correct":false,"tempting":[{"text":"A missing secret fails later, at the token exchange, with an error about the client, not about the redirect."}]},{"text":[{"text":"The callback address the deployed app sends is not on the list registered with Google, which still only contains the localhost one."}],"correct":true,"tempting":null},{"text":[{"text":"The browser is blocking third-party cookies."}],"correct":false,"tempting":[{"text":"Cookie blocking breaks the session after sign-in; the redirect check happens on Google’s side before any cookie is involved."}]}],"order":[3,1,2,0],"why":null},"prev":{"slug":"seed-data-and-the-empty-state","title":"Seed data, and the empty state nobody tested","minutes":7},"next":{"slug":"sessions-cookies-and-who-you-are","title":"Sessions, cookies, and who the server thinks you are","minutes":8}}}