
구글 로그인 구현하다가 3일 밤샌 썰 (OAuth 2.0의 원리와 보안)
회원가입 폼을 없애고 '구글로 로그인' 버튼을 달면 쉬울 줄 알았습니다. 하지만 Redirect URI 에러, State 파라미터, HTTPS 문제 등 OAuth 2.0의 복잡함에 압도당했죠. OAuth의 4단계 '댄스'와 실제적인 해결책(NextAuth.js), 그리고 모바일 앱에서의 딥링크 처리까지 깊이 있게 다룹니다.

회원가입 폼을 없애고 '구글로 로그인' 버튼을 달면 쉬울 줄 알았습니다. 하지만 Redirect URI 에러, State 파라미터, HTTPS 문제 등 OAuth 2.0의 복잡함에 압도당했죠. OAuth의 4단계 '댄스'와 실제적인 해결책(NextAuth.js), 그리고 모바일 앱에서의 딥링크 처리까지 깊이 있게 다룹니다.
프론트엔드 개발자가 알아야 할 4가지 저장소의 차이점과 보안 이슈(XSS, CSRF), 그리고 언제 무엇을 써야 하는지에 대한 명확한 기준.

Debug에선 잘 되는데 Release에서만 죽나요? 범인은 '난독화'입니다. R8의 원리, Mapping 파일 분석, 그리고 Reflection을 사용하는 라이브러리를 지켜내는 방법(@Keep)을 정리해봤습니다.

비트코인은 블록체인의 일부입니다. 이중 지불 문제(Double Spending), 작업 증명(PoW)과 지분 증명(PoS)의 차이, 스마트 컨트랙트, 그리고 Web 3.0이 가져올 미래까지. 개발자 관점에서 본 블록체인의 모든 것.

내 서버가 해킹당하지 않는 이유. 포트와 IP를 검사하는 '패킷 필터링'부터 AWS Security Group까지, 방화벽의 진화 과정.

서비스 런칭을 앞두고 있었습니다. 베타 테스터들에게서 "이메일/비밀번호 입력하기 귀찮아서 가입 안 해요"라는 피드백을 받았죠. 그래서 결정했습니다. "구글 로그인(OAuth)을 붙이자!"
문서 좀 보고 API 호출하면 끝날 줄 알았습니다.
하지만 현실은 400: redirect_uri_mismatch, invalid_grant 같은 알 수 없는 에러 메시지와의 전쟁이었습니다.
처음엔 OAuth가 그냥 "비밀번호 대신 키를 주는 것"인 줄 알았습니다. 하지만 알고 보니 이건 정해진 순서대로 스텝을 밟아야 하는 사교 댄스였습니다. 스텝이 하나라도 꼬이면 음악(로그인)은 멈춥니다.
사용자가 "구글 로그인" 버튼을 누릅니다. 제 앱(Client)이 구글(Provider)에게 말합니다.
"저기요, 이 사용자가 저랑 춤추고 싶다는데요? (로그인하고 싶대요)" "다 끝나면
http://localhost:3000/callback여기로 보내주세요." "그리고 저는 이 사람의profile만 볼래요. (Scope)"
구글이 사용자에게 묻습니다.
"이 앱이 님 이메일이랑 프로필 보겠다는데, 허락해요?"
사용자가 "허용"을 누르면 구글은 승낙의 표시로 임시 티켓(Authorization Code)을 줍니다. 이건 진짜 열쇠가 아닙니다. 그냥 "사용자가 허락했음"을 증명하는 종이쪼가리입니다.
이게 제일 헷갈렸던 부분입니다. 왜 바로 토큰을 안 주고 코드를 줄까요? 보안 때문입니다. 사용자의 브라우저(URL)를 통해 전달되는 코드는 누구나 훔쳐볼 수 있습니다.
그래서 제 서버(Server)가 구글 서버에게 몰래 가서 티켓을 내밉니다.
이때 Client Secret이라는 진짜 비밀번호를 같이 보여줍니다.
"아까 그 사용자가 준 티켓(Code)인데요. 저 진짜 이 앱 주인 맞거든요(Client Secret)? 진짜 열쇠(Access Token)로 바꿔주세요."
구글이 확인하고 진짜 열쇠(Access Token)를 줍니다. 이제 제 서버는 그 열쇠로 "사용자 이메일 내놔"라고 당당하게 요구할 수 있습니다.
이 댄스 스텝을 밟으면서 저는 세 번 넘어졌습니다.
redirect_uri_mismatch구글 개발자 콘솔에는 http://localhost:3000/api/auth/callback/google이라고 등록했습니다.
근데 실제로는 http://127.0.0.1:3000으로 접속해서 테스트했죠.
구글은 이 둘을 완전히 다른 사이트로 취급합니다.
교훈: 등록한 주소와 토씨 하나 안 틀리고 똑같아야 합니다. Trailing Slash(/) 유무도 검사합니다.
"이건 선택 사항(Optional)이라네? 귀찮으니까 패스." 나중에 알았습니다. 이게 없으면 CSRF 공격에 뚫립니다.
해커가 자기 구글 계정의 Authorization Code를 제 앱의 콜백 URL에 억지로 밀어넣습니다.
그러면 피해자는 자기도 모르게 해커의 계정으로 로그인하게 되고, 피해자가 저장한 신용카드 정보가 해커의 계정에 저장됩니다.
state는 "이 요청이 내가 보낸 게 맞다"라고 증명하는 난수(Random String)입니다.
웹에서는 잘 됐는데, React Native 앱을 만드니 또 안 됩니다.
앱은 URL이 없으니까요.
결국 myapp://callback 같은 커스텀 스킴(Custom Scheme)을 써야 했고, iOS/Android 설정을 따로 해줘야 했습니다.
모바일 앱이나 SPA(Single Page App)는 Client Secret을 가질 수 없습니다 (소스코드를 뜯어보면 다 나오니까요).
비밀번호 없이 어떻게 "나 주인 맞다"고 증명할까요?
여기서 PKCE (픽시라고 읽음)가 등장합니다.
code_verifier)을 만들고, 이걸 암호화(code_challenge)해서 구글에 보냅니다.code_verifier)을 보냅니다.이제 모바일 앱에서도 비밀번호 없이 안전하게 로그인할 수 있습니다.
3일 동안 맨땅에 헤딩하면서 OAuth 코드를 직접 짰습니다. 그런데 나중에 보니 NextAuth.js (지금의 Auth.js) 라는 라이브러리가 있더군요.
제가 300줄로 짠 코드를, 이 라이브러리는 설정 파일 10줄로 끝내버렸습니다.
/* pages/api/auth/[...nextauth].js */
import GoogleProvider from "next-auth/providers/google";
export default NextAuth({
providers: [
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
}),
],
callbacks: {
async jwt({ token, account }) {
if (account) token.accessToken = account.access_token;
return token;
},
},
});
허무했습니다. 하지만 동시에 안도했습니다. "이제 유지보수(토큰 갱신, 세션 관리, 보안 패치) 내가 안 해도 된다!"
OAuth 2.0의 원리(Code Flow, Refresh Token, PKCE)를 이해하는 건 중요합니다. 그래야 에러가 났을 때 어디가 문제인지, 보안 위협이 어디에 있는지 아니까요.
하지만 실제로 직접 구현하지는 마세요. 보안은 전문가들이 만든, 검증된 라이브러리(NextAuth, Supabase Auth, Clerk 등)를 쓰는 게 정신 건강에도, 사용자에게도 좋습니다.
여러분의 3일은 소중하니까요. 이제 그 시간으로 더 멋진 기능을 만드세요.
I was about to launch my service. Beta testers complained, "Can't I just use Google login? I hate typing passwords." So I decided. "Let's add OAuth!"
I thought I just needed to read the docs and call an API.
Reality was a war against 400: redirect_uri_mismatch, invalid_grant, and mysterious CORS errors.
I thought OAuth was just "giving a key instead of a password." But it turns out, it's a Social Dance with strict steps. Cross one leg wrong, and the music (login) stops.
User clicks "Login with Google". My App (Client) tells Google (Provider):
"Hey, this user wants to dance with me (login). When done, send them to
http://localhost:3000/callback. I only want to see theirprofile. (Scope)"
Google asks the user:
"This app wants to see your email and profile. Allow?"
User clicks "Allow". Google gives a Temporary Ticket (Authorization Code) as a sign of approval. This is NOT the key. It's just a slip of paper saying "User said yes".
This was the most confusing part. Why give a Code first, not the Token? Because of security. The user's browser (URL bar) is public territory. Any malicious extension could steal the token if it were sent there.
So My Server goes to Google's Server secretly (Back-channel) with the ticket.
I also show my ID card (Client Secret).
"Here's the ticket from that user. And here is my Secret ID. Give me the Real Key (Access Token)."
Google verifies the ticket and ID, then hands over the Access Token. Now my server can confidentally ask, "Give me the user's email," using that token.
While dancing, I tripped three times.
redirect_uri_mismatchI registered http://localhost:3000/api/auth/callback/google in Google Console.
But I tested using http://127.0.0.1:3000.
Google treats these as completely different sites.
Lesson: The URL must match EXACTLY, character for character. Even a trailing slash / matters.
"Docs say it's Optional? I'm lazy, I'll update it later." Later I learned this leaves me open to Login CSRF attacks.
A hacker forces their Authorization Code into my callback URL.
I unknowingly log in as the hacker. I enter my credit card info. It gets saved to the hacker's account.
The state parameter is a random string that proves "I started this request".
It worked on Web, but failed on React Native.
Apps don't have URLs like https://....
I had to register a Custom Scheme (myapp://callback) and configure iOS/Android manifest files. It was hellish.
Mobile apps and SPAs (Single Page Apps) cannot keep a Client Secret. (Anyone can decompile code or inspect network traffic).
So how do they prove "I am the owner" without a password?
Enter PKCE (Pronounced 'Pixy').
code_verifier) and encrypts it (code_challenge). Sends the encrypted version to Google.code_verifier) along with the Code.This allows public clients to do OAuth securely without secrets.
I spent 3 days writing raw OAuth code. Then I found NextAuth.js (Auth.js).
My 300 lines of spaghetti code were replaced by 10 lines of config.
/* pages/api/auth/[...nextauth].js */
import GoogleProvider from "next-auth/providers/google";
export default NextAuth({
providers: [
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
}),
],
callbacks: {
async jwt({ token, account }) {
if (account) token.accessToken = account.access_token;
return token;
},
},
});
I felt empty. But also relieved. "I don't have to maintain token refreshes, session storage, or security patches anymore!"
Understanding OAuth 2.0 principles (Code Flow, Refresh Token, PKCE) is crucial. You need it to debug when things go wrong.
But do not implement it yourself in production. Security is hard. Use battle-tested libraries (NextAuth, Supabase Auth, Clerk, etc.). It's better for your mental health and your users' safety.
Your 3 days are too precious. Go build something cool instead.
If you are stuck, check this checklist.
redirect_uri_mismatchhttp://localhost:3000 AND http://127.0.0.1:3000?/api/auth/callback/google)/ at the end matters!)https in production?google-services.json (Android) and GoogleService-Info.plist (iOS)?com.myapp://?OAuth is hard because it's strict. But once you get it right, it's the gold standard for authentication. Don't give up!