blog.itcode.devblog.itcode.dev

[OAuth2.0] Building an OAuth2.0 Authorization Server with ScribeJAVA - 6. Applying for the KAKAO OAuth Service and Implementing the Module

As the third platform, let's apply for an OAuth service with KAKAO and implement the authentication module.

[OAuth2.0] Building an OAuth2.0 Authorization Server with ScribeJAVA - 6. Applying for the KAKAO OAuth Service and Implementing the Module

As the third platform, let's apply for an OAuth service with KAKAO and implement the authentication module.
RWB0104
@RWBwritten at 2021-10-22 18:51:04
Building an OAuth2.0 Authorization Server

시리즈 모아보기

Building an OAuth2.0 Authorization Server

6 / 11

As the third platform, let's apply for an OAuth service with KAKAO and implement the authentication module.

Let's apply for the KAKAO OAuth service to obtain the API information.

After logging in, go to Kakao Developers.

You can check the list of applications in [My Applications] in the top menu.

Let's create an application that will manage the OAuth information. The name doesn't matter — pick anything you like. Business information is also a required field, so if you're not a registered business, just enter something random or the same as the name. You can register the logo later too.

To create an OAuth API, you first need to configure the consent screen. Yes, that's the consent to provide information.

You can configure it in the [Kakao Login - Consent Items] menu in the left sidebar. The configurable states are as below.

  • Required consent - an item that must be consented to. Login isn't possible without consenting to this item.
  • Optional consent - an item the user can choose to consent to. Whether or not they consent doesn't affect login.
  • Consent while in use - not shown at login time. Consent is requested separately later via the API when needed.
  • Not used - not used.

Unlike Naver, which doesn't force the user to consent to anything, Kakao is set up so that login isn't possible unless the user consents to the required items.

Specify nickname, profile picture, and email. Note that email can only be set as required for business apps.

What's a business app?
This is a concept similar to KAKAO's OAuth application review. By default, it requires things like a business registration number, so an individual without one has to apply for it separately. This will be covered in detail during the review stage later on.

Specify the redirect URI to which code will be delivered after login.

You can configure it in the [Kakao Login] menu in the left sidebar.

You can register multiple URLs, separated by line breaks.

In the [Kakao Login - Security] menu in the left sidebar, enable Client Secret.

You must enable this in order to use the Secret key.

In the [Kakao Login] menu in the left sidebar, you can activate the application by checking Activation.

You can check it directly on the application's main [Summary] page.

  • Native App Key - for mobile
  • REST API Key - for HTTP requests
  • JavaScript Key - for the SDK
  • Admin Key - an administrative key that integrates all of the above functions

Use the REST API Key.

Now that all the necessary preparations are in place, let's implement the Kakao authentication module. We'll implement it by extending the previously implemented AuthModule.

JAVA

public class KakaoAuthModule extends AuthModule
{
	// KAKAO authentication module
}

The basic form of the object is as above.

MethodMethod TypeDescriptionImplementation Needed?
getAuthorizationUrlabstractReturns the authentication URLY
getAccessTokenReturns the access tokenY
getRefreshAccessTokenRefreshes and returns the access token
getUserInfoReturns the user info response
getRefreshTokenEndpointReturns the access token reissue request URL
getApiKeyBeanReturns the API key object
getUserInfoEndPointReturns the user info request URL
getUserInfoBeanabstractReturns the user info objectY
deleteInfoabstractReturns the result of unlinkingY
getUpdateAuthorizationUrlabstractReturns the URL for renewing consent to provide informationY
getAccessTokenEndpointabstractReturns the access token request URLY
getAuthorizationBaseUrlabstractReturns the authentication API request URLY

What the Kakao module needs to implement is as above. Unlike the previous platforms, getAccessToken needs to be overridden.

Create a kakao.properties file under WEB-INF. You can also copy the already-created sample.properties and use that.

PROPERTIES

api=API_KEY
secret=SECRET_KEY
callback=CALLBACK_URL

The basic format is as above; just enter the appropriate values for each item.

For the authentication module to work correctly, there are certain methods and variables that need to be set up by default, such as configuring API information and returning the instance.

JAVA

private static final String MODULE_NAME = "kakao";

private static final String API_KEY;
private static final String SECRET_KEY;
private static final String CALLBACK_URL;

static
{
	ApiKeyBean apiKeyBean = getApiKeyBean(MODULE_NAME);
	
	API_KEY = apiKeyBean.getApi();
	SECRET_KEY = apiKeyBean.getSecret();
	CALLBACK_URL = apiKeyBean.getCallback();
}

private static final ServiceBuilderOAuth20 SERVICE_BUILDER = new ServiceBuilder(API_KEY).apiSecret(SECRET_KEY).callback(CALLBACK_URL);

private static final KakaoAuthModule INSTANCE = new KakaoAuthModule(SERVICE_BUILDER);

private KakaoAuthModule(ServiceBuilderOAuth20 serviceBuilder)
{
	super(serviceBuilder);
}

public static KakaoAuthModule getInstance()
{
	return INSTANCE;
}

For Google, scope must always be specified. Just plug in the scope you selected in 3-3. Specifying Scopes as the scope used here.

FieldTypeDescription
MODULE_NAMEStringThe module name
API_KEYStringThe API key
SECRET_KEYStringThe secret key
CALLBACK_URLStringThe callback URL
SERVICE_BUILDERServiceBuilderOAuth20The OAuth2.0 service builder
INSTANCEKakaoAuthModuleThe instance

All the defined variables are declared as static final, so they're declared only once when the instance is created and cannot be reassigned.

Through the static{ } block, the API information is set up when the instance is created.

When assigning the API values, the getApiKeyBean() method parses the properties with the given name and returns an ApiKeyBean object, which is then used.

Let's implement the methods that return the request URL for each API.

JAVA

@Override
public String getAccessTokenEndpoint()
{
	return "https://kauth.kakao.com/oauth/token";
}

@Override
protected String getAuthorizationBaseUrl()
{
	return "https://kauth.kakao.com/oauth/authorize";
}

@Override
protected String getUserInfoEndPoint()
{
	return "https://kapi.kakao.com/v2/user/me";
}
  • getAccessTokenEndpoint() - token-related APIs use the URL returned by this method.
  • getAuthorizationBaseUrl() - authentication-related APIs use the URL returned by this method.
  • getUserInfoEndPoint() - user info-related APIs use the URL returned by this method.

These are the URLs needed to carry out OAuth2.0 operations. Among these, getAccessTokenEndpoint() and getAuthorizationBaseUrl() are abstract methods of DefaultApi20, an object from the scribeJAVA library, and the remaining one is an abstract method of AuthModule.

DefaultApi20 doesn't provide a separate method related to the user account API. However, since the user account API is essential when using AuthModule's common method for fetching user info, it's managed as an abstract method of AuthModule.

Let's implement the feature that returns the Kakao platform login URL.

First, let's look at the API.


  • Request

TXT

GET https://kauth.kakao.com/oauth/authorize?response_type=code&client_id={:client_id}&redirect_uri={:redirect_uri}&state={:state}
parametertypedatarequireddescription
{:response_type}pathStringYThe response type. Fixed to code
{:client_id}pathStringYThe API key
{:redirect_uri}pathStringYThe Callback URL
{:state}pathStringYA unique state value

  • Response

The Kakao platform login page


The Kakao platform login API is as above. You just need to design the method to return the request URL.

You could build the URL directly with string operations, but you can easily generate the URL using the service.getAuthorizationUrl() method.


Since it's already declared as a common method in AuthModule, there's no need to implement it separately.

Since we receive the Code as the login result, we'll implement the feature that exchanges it for an Access Token.

The Kakao API is as follows. Either scribeJAVA doesn't handle Kakao correctly, or something else is off, but using the same common interface causes the parameters not to be entered properly, resulting in an error. Because of this, we have no choice but to build and send the request directly.


  • Request

TXT

POST https://kauth.kakao.com/oauth/token?grant_type=authorization_code&client_id={:client_id}&client_secret={:client_secret}&redirect_uri={:redirect_uri}&code={:code}
parametertypedatarequireddescription
{:grant_type}pathStringYThe grant type. Fixed to authorization_code
{:client_id}pathStringYThe API key
{:client_secret}pathStringYThe secret key
{:redirect_uri}pathStringYThe Callback URL
{:code}pathStringYThe authorization code

  • Response

JSON

{
	"token_type": "bearer",
	"access_token": "{ACCESS_TOKEN}",
	"expires_in": 43199,
	"refresh_token": "{REFRESH_TOKEN}",
	"refresh_token_expires_in": 25184000,
	"scope": "account_email profile"
}
parameterdatadescription
access_tokenStringThe access token
refresh_tokenStringThe refresh token
refresh_token_expires_inintThe refresh token's expiration time (in seconds)
token_typeStringThe token type
expires_inintThe expiration time (in seconds)
scopeStringThe access permissions

The request is built with an AccessTokenRequestParams object and the response is received via service.getAccessToken.

Because of the library's handling issue, we have to use a separately overridden method rather than AuthModule's common method.

The Access Token has a very short expiration time, about one hour. When the Access Token expires, normally the user would need to be asked to re-authenticate via platform login, but if a Refresh Token is available, you can reissue the Access Token internally in the service without going through any extra steps.

This Refresh Token doesn't carry authentication rights, but it does carry the right to reissue an Access Token.

The Kakao API implementing this is as follows.


  • Request

TXT

POST https://kauth.kakao.com/oauth/token?grant_type=refresh_token&client_id={:client_id}&client_secret={:client_secret}&refresh_token=${:refresh_token}
parametertypedatarequireddescription
{:grant_type}pathStringYThe grant type. Fixed to refresh_token
{:client_id}pathStringYThe API key
{:client_secret}pathStringYThe secret key
{:refresh_token}pathStringYThe refresh token

  • Response

JSON

{
	"access_token": "{ACCESS_TOKEN}",
	"token_type": "bearer",
	"refresh_token": "{REFRESH_TOKEN}",
	"refresh_token_expires_in": 25184000,
	"expires_in": 43199,
}
parameterdatadescription
access_tokenStringThe access token
token_typeStringThe token type
refresh_tokenStringThe refresh token
refresh_token_expires_inintThe refresh token's expiration time (in seconds)
expires_inintThe expiration time (in seconds)

Since this can be replaced by AuthModule's common method, it's not implemented separately.

Let's implement the feature that fetches user info using the Access Token. This is the part where the issued Access Token is actually put to meaningful use.

The Kakao API is as follows.


  • Request

TXT

GET/POST https://kapi.kakao.com/v2/user/me
Authorization: Bearer {:access_token}
parametertypedatarequireddescription
{:access_token}headerStringYThe access token

  • Response

JSON

{
	"id":123456789,
	"kakao_account": { 
		"profile_needs_agreement": false,
		"profile": {
			"nickname": "홍길동",
			"thumbnail_image_url": "http://yyy.kakao.com/.../img_110x110.jpg",
			"profile_image_url": "http://yyy.kakao.com/dn/.../img_640x640.jpg",
			"is_default_image": false
		},
		"email_needs_agreement": false, 
		"is_email_valid": true,   
		"is_email_verified": true,   
		"email": "sample@sample.com",
		"age_range_needs_agreement": false,
		"age_range": "20~29",
		"birthday_needs_agreement": false,
		"birthday": "1130",
		"gender_needs_agreement": false,
		"gender": "female"
	},  
	"properties": {
		"nickname": "홍길동카톡",
		"thumbnail_image": "http://xxx.kakao.co.kr/.../aaa.jpg",
		"profile_image": "http://xxx.kakao.co.kr/.../bbb.jpg",
		"custom_field1": "23",
		"custom_field2": "여"
	}
}

The response spec is very extensive, so it's omitted here. Refer to the Kakao Developer docs.


The id is not the xxx@google.com-style ID we normally think of, but a unique hash value assigned per account.

Since this can be replaced by AuthModule's common method, it's not implemented separately.

Let's implement the method that parses the response according to Kakao's user info API response format and returns it as a UserInfoBean.

This project only uses the name, email, and profile picture URL, so we extract those values from the response and store them in the object.


  • Code

JAVA

@Override
public UserInfoBean getUserInfoBean(String body) throws JsonProcessingException
{
	ObjectMapper mapper = new ObjectMapper();
	
	JsonNode node = mapper.readTree(body);
	
	String email = node.get("kakao_account").get("email") == null ? "미동의" : node.get("kakao_account").get("email").textValue();
	String name = node.get("kakao_account").get("profile").get("nickname") == null ? "미동의" : node.get("kakao_account").get("profile").get("nickname").textValue();
	String picture = node.get("kakao_account").get("profile").get("profile_image_url") == null ? "/oauth2/assets/images/logo.png" : node.get("kakao_account").get("profile").get("profile_image_url").textValue();
	
	return new UserInfoBean(email, name, picture, MODULE_NAME);
}

We extract the needed values according to the response format. If the user didn't consent to providing the information, the target object returns null. To prevent errors from missing data, null handling must always be done for the data.

The first time you log in with a Kakao ID, you go through consent to provide information, but on subsequent logins, this consent step is skipped. In other words, this means the platform saves the consent to provide information somewhere the first time you log in. If a user withdraws their membership from the service, it's necessary to unlink from Kakao and completely delete the information.

The Kakao API is as follows.


  • Request

TXT

POST https://kapi.kakao.com/v1/user/unlink
Authorization: Bearer {:access}
Content-Type: application/x-www-form-urlencoded
parametertypedatarequireddescription
{:access}headerStringYThe access token

  • Response

JSON

{
	"id": 123456789
}
parameterdatadescription
idlongThe member number

  • Code

JAVA

@Override
public boolean deleteInfo(String access) throws IOException, ExecutionException, InterruptedException
{
	OAuthRequest oAuthRequest = new OAuthRequest(Verb.POST, "https://kapi.kakao.com/v1/user/unlink");
	oAuthRequest.addHeader("Content-Type", "application/x-www-form-urlencoded");
	oAuthRequest.addHeader("Authorization", Util.builder("Bearer ", access));
	
	service.signRequest(access, oAuthRequest);
	
	return service.execute(oAuthRequest).isSuccessful();
}

The implementation is simple. Using the OAuthRequest object, you can easily build the request. The response body itself doesn't matter. We just use response.isSuccessful() to determine whether the response is normal and return that as a boolean.

While the service is running, if additional user information becomes necessary or unnecessary, you can reset the consent information by renewing user info consent.

For Kakao, only optional items that haven't been consented to can be newly consented to; data that's already been consented to must be revoked through a separate API.


  • Request

TXT

GET https://kauth.kakao.com/oauth/authorize?response_type=code&client_id={:client_id}&redirect_uri={:redirect_uri}&state={:state}&scope={:scope}
parametertypedatarequireddescription
{:response_type}pathStringYThe response type. Fixed to code
{:client_id}pathStringYThe API key
{:redirect_uri}pathStringYThe Callback URL
{:state}pathStringYA unique state value
{:scope}pathStringYThe access permissions

  • Code

JAVA

@Override
public String getUpdateAuthorizationUrl(String state)
{
	HashMap<String, String> params = new HashMap<>();
	params.put("state", state);
	params.put("scope", "profile_nickname,profile_image,account_email");
	
	return service.getAuthorizationUrl(params);
}

We return null so that no action is performed. The process that follows handles the case where a null value is returned separately.

JAVA

package oauth.account.module;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.scribejava.core.builder.ServiceBuilder;
import com.github.scribejava.core.builder.ServiceBuilderOAuth20;
import com.github.scribejava.core.model.OAuth2AccessToken;
import com.github.scribejava.core.model.OAuthRequest;
import com.github.scribejava.core.model.Verb;
import com.github.scribejava.core.oauth.AccessTokenRequestParams;
import global.module.Util;
import oauth.account.bean.ApiKeyBean;
import oauth.account.bean.UserInfoBean;

import java.io.IOException;
import java.util.HashMap;
import java.util.concurrent.ExecutionException;

/**
 * 카카오 인증 모듈 클래스
 *
 * @author RWB
 * @since 2021.10.04 Mon 21:30:49
 */
public class KakaoAuthModule extends AuthModule
{
	private static final String MODULE_NAME = "kakao";
	
	private static final String API_KEY;
	private static final String SECRET_KEY;
	private static final String CALLBACK_URL;
	
	static
	{
		ApiKeyBean apiKeyBean = getApiKeyBean(MODULE_NAME);
		
		API_KEY = apiKeyBean.getApi();
		SECRET_KEY = apiKeyBean.getSecret();
		CALLBACK_URL = apiKeyBean.getCallback();
	}
	
	private static final ServiceBuilderOAuth20 SERVICE_BUILDER = new ServiceBuilder(API_KEY).apiSecret(SECRET_KEY).callback(CALLBACK_URL);
	
	private static final KakaoAuthModule INSTANCE = new KakaoAuthModule(SERVICE_BUILDER);
	
	/**
	 * 생성자 메서드
	 *
	 * @param serviceBuilder: [ServiceBuilderOAuth20] API 서비스 빌더
	 */
	private KakaoAuthModule(ServiceBuilderOAuth20 serviceBuilder)
	{
		super(serviceBuilder);
	}
	
	/**
	 * 인스턴스 반환 메서드
	 *
	 * @return [KakaoAuthModule] 인스턴스
	 */
	public static KakaoAuthModule getInstance()
	{
		return INSTANCE;
	}
	
	/**
	 * 접근 토큰 반환 메서드
	 *
	 * @param code: [String] 인증 코드
	 *
	 * @return [OAuth2AccessToken] 접근 토큰
	 *
	 * @throws IOException 데이터 입출력 예외
	 */
	@Override
	public OAuth2AccessToken getAccessToken(String code) throws IOException, ExecutionException, InterruptedException
	{
		AccessTokenRequestParams params = new AccessTokenRequestParams(code);
		params.addExtraParameter("client_id", API_KEY);
		params.addExtraParameter("client_secret", SECRET_KEY);
		
		return getAccessToken(params);
	}
	
	/**
	 * 유저 정보 객체 반환 메서드
	 *
	 * @param body: [String] OAuth 응답 내용
	 *
	 * @return [UserInfoBean] 유저 정보 객체
	 *
	 * @throws JsonProcessingException JSON 파싱 예외
	 */
	@Override
	public UserInfoBean getUserInfoBean(String body) throws JsonProcessingException
	{
		ObjectMapper mapper = new ObjectMapper();
		
		JsonNode node = mapper.readTree(body);
		
		String email = node.get("kakao_account").get("email") == null ? "미동의" : node.get("kakao_account").get("email").textValue();
		String name = node.get("kakao_account").get("profile").get("nickname") == null ? "미동의" : node.get("kakao_account").get("profile").get("nickname").textValue();
		String picture = node.get("kakao_account").get("profile").get("profile_image_url") == null ? "/oauth2/assets/images/logo.png" : node.get("kakao_account").get("profile").get("profile_image_url").textValue();
		
		return new UserInfoBean(email, name, picture, MODULE_NAME);
	}
	
	/**
	 * 연동 해제 결과 반환 메서드
	 *
	 * @param access: [String] 접근 토큰
	 *
	 * @return [boolean] 연동 해제 결과
	 *
	 * @throws IOException 데이터 입출력 예외
	 * @throws ExecutionException 실행 예외
	 * @throws InterruptedException 인터럽트 예외
	 */
	@Override
	public boolean deleteInfo(String access) throws IOException, ExecutionException, InterruptedException
	{
		OAuthRequest oAuthRequest = new OAuthRequest(Verb.POST, "https://kapi.kakao.com/v1/user/unlink");
		oAuthRequest.addHeader("Content-Type", "application/x-www-form-urlencoded");
		oAuthRequest.addHeader("Authorization", Util.builder("Bearer ", access));
		
		service.signRequest(access, oAuthRequest);
		
		return service.execute(oAuthRequest).isSuccessful();
	}
	
	/**
	 * 정보 제공 동의 갱신 URL 반환 메서드
	 *
	 * @param state: [String] 고유 상태값
	 *
	 * @return [String] 정보 제공 동의 갱신 URL
	 */
	@Override
	public String getUpdateAuthorizationUrl(String state)
	{
		HashMap<String, String> params = new HashMap<>();
		params.put("state", state);
		params.put("scope", "profile_nickname,profile_image,account_email");
		
		return service.getAuthorizationUrl(params);
	}
	
	/**
	 * 접근 토큰 요청 URL 반환 메서드
	 *
	 * @return [String] 접근 토큰 요청 URL
	 */
	@Override
	public String getAccessTokenEndpoint()
	{
		return "https://kauth.kakao.com/oauth/token";
	}
	
	/**
	 * 인증 API 요청 URL 반환 메서드
	 *
	 * @return [String] 인증 API 요청 URL
	 */
	@Override
	protected String getAuthorizationBaseUrl()
	{
		return "https://kauth.kakao.com/oauth/authorize";
	}
	
	/**
	 * 사용자 정보 요청 URL 반환 메서드
	 *
	 * @return [String] 사용자 정보 요청 URL
	 */
	@Override
	protected String getUserInfoEndPoint()
	{
		return "https://kapi.kakao.com/v2/user/me";
	}
}

The full, organized code is as above.

This is very similar to the implementation of Naver's authentication module. The methods that need to be implemented or overridden are all the same. Through AuthModule, replacing things with a common module and overriding when necessary made it clear just how effectively you can adapt to multiple platforms this way. I got a renewed appreciation for why object orientation is so useful for maintainability.

With this, the implementation of the Google authentication module is complete. At this stage, since it's still in development, it can only be used with designated test IDs. You need to register a test account in the API settings in order to test login with that account. Once the application is approved after review, login will be available with all IDs.

# JAVA# OAuth2.0# scribeJAVA
ship
blog.itcode.dev

Notes from the π-th Alpaca

7.0.1
Developed by RWB since 2021.057th upgraded at 2026.08