들어가며#
✅ 해당 프로젝트는 Boilerplate 템플릿 https://github.com/cha2hyun/nestjs-prisma-starter-kakao-oauth-jwt 으로 제작되어있습니다.
Don’t reinvent the wheel!
NestJS 공식문서 에도 기재되어있는 무려 2,000 스타수가 넘는 notiz-dev/nestjs-prisma-starter 를 기반으로 카카오 OAUTH 부분만 추가해두었습니다.
하다보니 에러가 있어서 pr 보냈더니 merged 되었다🖐️ 스타수 2K 기여했다는 이 뿌듯함..
카카오 OAUTH는 대부분 한국에서 사용되겠지만, 그래도 범용(?)적으로 처음으로 영문으로 Readme도 적어보았습니다.
Instructions#
🚀 This project is generated from notiz-dev/nestjs-prisma-starter starter template which is referenced in the NestJS official documentation . If you need more information, such as installation and setup, please check README within the template.
👀 This project provides Kakao Oauth login with Passport JWT authentication.
📝 Feel free to let me know if encounter any errors or have any questions. Pull requests are welcome.
Features#
login/signup with kakao account with JWT tokens
Overview#
1. Setup a kakao sdk in your frontend.#
Please check kakao documents for setup. In my case i use next.js for the example.
2. Login with kakao account.#
Here are some Next.js frontend code examples
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
< Script
src = "https://t1.kakaocdn.net/kakao_js_sdk/2.1.0/kakao.min.js"
integrity = "sha384-dpu02ieKC6NUeKFoGMOKz6102CLEWi9+5RQjWSV0ikYSFFd8M3Wp2reIcquJOemx"
crossOrigin = "anonymous"
onReady = {() => {
if ( ! ( "Kakao" in window )) return ;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if (( window as any ). Kakao == null ) return ;
if ( process . env . NEXT_PUBLIC_KAKAO_JS_KEY == null ) return ;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const kakao = ( window as any ). Kakao ;
if ( kakao . isInitialized () !== true ) {
kakao . init ( process . env . NEXT_PUBLIC_KAKAO_JS_KEY );
}
}}
/>
Add kakao sdk on _app.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
const handleLogin = useCallback ( async () => {
if ( ! ( "Kakao" in window )) return ;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if (( window as any ). Kakao == null ) return ;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const kakao = ( window as any ). Kakao ;
kakao . Auth . authorize ({
redirectUri :
typeof window !== "undefined"
? window . location . origin + "/auth/kakao"
: null ,
prompts : "login" ,
});
}, []);
In this case redirect url is /auth/kakao
3. Get Code Parameter.#
If your account passes the login, the browser will redirect to your redirectUri
with code
parameters.
The URL will look like http://localhost:3002/auth/kakao?code=TJx7M1-sTWkrKQgvOTmfvSUnC5bD2GqtWrA....
On your redirect page, initiate a login mutation to the NestJS server using code
and redirectUri
as variables.
For instance, here are some Next.js frontend code snippets.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
/* eslint-disable no-console */
import { Spinner } from "@nextui-org/react" ;
import { NextPage } from "next" ;
import { useRouter } from "next/router" ;
import { useEffect , useState } from "react" ;
import { useLoginMutation } from "@/src/core/config/graphql" ;
import Authentication from "@/src/core/function/authentication" ;
const KakaoOauth : NextPage = () => {
const router = useRouter ();
const [ login ] = useLoginMutation ();
const [ isFetched , setIsFetched ] = useState ( false );
useEffect (() => {
const params = new URL ( document . location . toString ()). searchParams ;
const code = params . get ( "code" );
const fetchData = async () => {
try {
if ( code && typeof window !== undefined && ! isFetched ) {
await login ({
variables : {
code : code ,
redirectUri : window.location.origin + "/auth/kakao" ,
},
onCompleted : async ( res ) => {
setIsFetched ( true );
Authentication . setToken ({
accessToken : res.login.accessToken ,
refreshToken : res.login.refreshToken ,
});
console . log ( "jwt" , res . login . accessToken );
},
});
}
} catch ( err ) {
console . log ( err );
}
};
fetchData ();
}, [ isFetched , login , router ]);
return (
< div className = "bg-defualt my-20 flex w-full justify-center " >
< div className = "mx-auto w-full flex-1 text-center " >
< Spinner label = "Waiting for server response..." color = "warning" />
</ div >
</ div >
);
};
export default KakaoOauth ;
auth/kakao.tsx
5. Returning JWT Token on the NestJS server#
First, auth.reslover.ts
#
1
2
3
4
5
6
7
8
9
10
11
@Mutation (() => Auth )
async login (
@Args ( "code" ) code : string ,
@Args ( "redirectUri" )
redirectUri : string ,
) {
const { accessToken , refreshToken } = await this . auth . kakaoLogin ( code , redirectUri );
return {
accessToken ,
refreshToken ,
};
src > auth > auth.resolver.ts
it will call kakaoLogin functions in auth.service.ts
Second in auth.service.ts
It will fetch the kakao access token
using the code
and redirectUri
parameters.#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
async kakaoLogin ( code : string , redirectUri : string ) : Promise < Token > {
try {
const tokenResponse = await this . kakaoLoginService . getToken ( code , redirectUri );
const kakaoUser = await this . kakaoLoginService . getUser ( tokenResponse . access_token );
const isJoined = await this . prisma . user . findUnique ({ where : { kakaoId : kakaoUser.id.toString () } });
if ( ! isJoined ) {
return this . createUser ({
kakaoId : kakaoUser.id.toString (),
email : kakaoUser.kakao_account.email ,
nickname : kakaoUser.properties.nickname ,
connectedAt : kakaoUser.connected_at ,
ageRange : kakaoUser.kakao_account.age_range ,
birthday : kakaoUser.kakao_account.birthday ,
gender : kakaoUser.kakao_account.gender ,
profileImageUrl : kakaoUser.properties.profile_image ,
thumbnailImageUrl : kakaoUser.properties.thumbnail_image ,
});
}
else {
const userId = ( await this . prisma . user . findUnique ({ where : { kakaoId : kakaoUser.id.toString () } })). id ;
return this . generateTokens ({
userId : userId ,
});
}
} catch ( e ) {
throw new Error ( e );
}
}
src > auth > auth.service.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
async getToken ( code : string , redirectUri : string ) : Promise < KakaoOauthToken > {
try {
const url = new URL ( "/oauth/token" , KAKAO_AUTH_URL ). href ;
const tokenResponse = await new Promise < KakaoOauthToken >(( resolve , reject ) => {
this . httpService
. post (
url ,
new URLSearchParams ({
grant_type : "authorization_code" ,
client_id : this.configService.get ( "KAKAO_API_CLIENT_ID" ),
client_secret : this.configService.get ( "KAKAO_API_CLIENT_SECRET" ),
code : code ,
redirect_uri : redirectUri ,
}),
{
headers : {
"Content-Type" : "application/x-www-form-urlencoded" ,
},
},
)
. subscribe ({
error : err => reject ( err ),
next : response => resolve ( response . data ),
});
});
return tokenResponse ;
} catch ( e ) {
const err = e as AxiosError ;
// eslint-disable-next-line no-console
console . log ( err );
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
async getUser ( token : string ) {
try {
const url = new URL ( "/v2/user/me" , KAKAO_API_URL ). href ;
const infoResponse = await new Promise < KakaoV2UserMe >(( resolve , reject ) => {
this . httpService
. get ( url , {
headers : {
Authorization : `Bearer ${ token } ` ,
"Content-type" : "Content-type: application/x-www-form-urlencoded;charset=utf-8" ,
},
params : {
secure_resource : true ,
},
})
. subscribe ({
error : err => reject ( err ),
next : response => resolve ( response . data ),
});
});
return infoResponse ;
} catch ( e ) {
throw new UnauthorizedException ();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
async createUser ( payload : SignupInput ) : Promise < Token > {
try {
const user = await this . prisma . user . create ({
data : {
kakaoId : payload.kakaoId ,
email : payload.email ,
nickname : payload.nickname ,
ageRange : payload.ageRange ,
birthday : payload.birthday ,
gender : payload.gender ,
role : "USER" ,
kakaoProfile : {
create : {
profileImageUrl : payload.profileImageUrl ,
thumbnailImageUrl : payload.thumbnailImageUrl ,
connectedAt : payload.connectedAt ,
},
},
},
});
return this . generateTokens ({
userId : user.id ,
});
} catch ( e ) {
throw new Error ( e );
}
}
Fifthly, if the user already exists on database. It will return a JWT token genertated with the userId
#
6. Saving Returned JWT Token on your frontend#
The token will be returned to frontend. ensure to save it within the browser.
Token will be like
1
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbG95aDBydXIwMDAwM2hvYm9taTl3N21iIiwiaWF0IjoxNzAwMDM2MTA4LCJleHAiOjE3MDAwMzYyMjh9.M1YjIJcKjeUfYo4P8Humh7fAtc4PAxRI54tJAJDP
7. Execute the ‘Me’ query using JWT tokens.#
Add Authorization header with your JWT tokens to retrieve ‘Me’
Authorization : Bearer "YOUR JWT TOKENS"
on success
with worng token
8. Begin building your own projects 🚀#
마치며#
처음으로 제작한 Boilerplate template. 고작 한 기능이 추가되었지만 하면서 여러 공부도 되었고 공식문서에 적혀있는 기존 템플릿에 PR도 Merged 되어서 매우 뿌듯합니다. 누군가에게 언젠가 사용되길 바랍니다 😀
템플릿 바로가기 : https://github.com/cha2hyun/nestjs-prisma-starter-kakao-oauth-jwt