[OAuth2.0] Building an OAuth2.0 Authorization Server with ScribeJAVA - 4. Applying for the NAVER OAuth Service and Implementing the Module
[OAuth2.0] Building an OAuth2.0 Authorization Server with ScribeJAVA - 4. Applying for the NAVER OAuth Service and Implementing the Module
As the first platform, let's apply for an OAuth service with NAVER and implement the authentication module.
Let's apply for the NAVER OAuth service to obtain the API information.
After logging in, go to the Naver Developer Center.
NAVER provides its OAuth service under the name "Naver ID Login," commonly abbreviated as "Neh-aro" (네아로).
You just need to register an application that uses this service in the Naver Developer Center.
Go to the [Application - Register Application] menu in the top header.
Let's register an application that will manage the OAuth information.
You must fill in all the fields below, and these fields can be changed at any time even after review.
This is the name of the application. This name is also displayed in the Naver login window.
An example of my Naver login window. My application's name is OAuth2, and the name specified there is displayed in the OAuth2 part of the window above.
Select the API you want to use.
Naver provides various APIs, but for now, in line with the purpose of this project, select the Naver ID Login service.
Once you select the Naver ID Login service, a form for selecting the data to be provided is added.
This lets you select what information can be obtained from the user's authentication data, and you can classify each item as required or optional.
Information marked as required or optional can be accessed later when calling the user's information.
🛑 Caution!!
With Naver, even required information can be arbitrarily refused by the user. In other words, even if you notify the user that this information is required for the service to operate, if the user refuses it, there's nothing the service can do about it.
For this reason, verifying user information matters more with Naver than with other platforms. The difference between required and optional comes down to whether the checkbox defaults to checked or unchecked. Required items are checked by default, while optional items are shown to the user unchecked by default.
Select the information you want. If it's absolutely necessary, mark it as required; otherwise, leave it as optional. Don't check everything "just in case." It's not impossible to do so, but later during the review for activating full OAuth key distribution, requesting unnecessary or excessive data access permissions could result in the review being rejected. Only request the minimum information that's truly necessary.
Enter the environment in which OAuth will be used.
Since this project is built for the web, select PC Web. You can also add Android or iOS. You can add multiple environments at once and manage access to multiple platforms with a single key in an integrated way.
The service URL is the URL of the service to which you're applying Naver ID Login.
The Callback URL is the URL to which the login result will be delivered. You can specify up to 5, and the callback value in the naver.properties file you'll configure later must be one of these registered Callback URLs. If an unregistered Callback URL is detected during the platform login process, an error is returned.
Enter it in a URL format like https://example.com/oauth2, and you can append arbitrary URL parameters like https://example.com/oauth2?key=value.
Once everything looks good, create the application.
Once you create the application, you can see the API and Secret. The Secret is masked by default, and you can view it by clicking a separate button. If you believe the Secret has been leaked, you can reissue it. In that case, of course, you must apply the changed Secret to the authorization server.
Now that all the necessary preparations are in place, let's implement the NAVER authentication module. We'll implement it by extending the previously implemented AuthModule.
JAVA
public class NaverAuthModule extends AuthModule { // NAVER authentication module }
The basic form of the object is as above.
| Method | Method Type | Description | Implementation Needed? |
|---|---|---|---|
| getAuthorizationUrl | abstract | Returns the authentication URL | Y |
| getAccessToken | Returns the access token | ||
| getRefreshAccessToken | Refreshes and returns the access token | ||
| getUserInfo | Returns the user info response | ||
| getRefreshTokenEndpoint | Returns the access token reissue request URL | ||
| getApiKeyBean | Returns the API key object | ||
| getUserInfoEndPoint | Returns the user info request URL | ||
| getUserInfoBean | abstract | Returns the user info object | Y |
| deleteInfo | abstract | Returns the result of unlinking | Y |
| getUpdateAuthorizationUrl | abstract | Returns the URL for renewing consent to provide information | Y |
| getAccessTokenEndpoint | abstract | Returns the access token request URL | Y |
| getAuthorizationBaseUrl | abstract | Returns the authentication API request URL | Y |
The above is what the NAVER module needs to implement.
Create a naver.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 = "naver"; 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 NaverAuthModule INSTANCE = new NaverAuthModule(SERVICE_BUILDER); private NaverAuthModule(ServiceBuilderOAuth20 serviceBuilder) { super(serviceBuilder); } public static NaverAuthModule getInstance() { return INSTANCE; }
| Field | Type | Description |
|---|---|---|
| MODULE_NAME | String | The module name |
| API_KEY | String | The API key |
| SECRET_KEY | String | The secret key |
| CALLBACK_URL | String | The callback URL |
| SERVICE_BUILDER | ServiceBuilderOAuth20 | The OAuth2.0 service builder |
| INSTANCE | NaverAuthModule | The 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://nid.naver.com/oauth2.0/token"; } @Override protected String getAuthorizationBaseUrl() { return "https://nid.naver.com/oauth2.0/authorize"; } @Override protected String getUserInfoEndPoint() { return "https://openapi.naver.com/v1/nid/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 Naver platform login URL.
First, let's look at the API.
- Request
TXT
GET/POST https://nid.naver.com/oauth2.0/authorize?response_type=code&client_id={:client_id}&redirect_uri={:redirect_uri}&state={:state}
| parameter | type | data | required | description |
|---|---|---|---|---|
| {:response_type} | path | String | Y | The response type. Fixed to code |
| {:client_id} | path | String | Y | The API key |
| {:redirect_uri} | path | String | Y | The Callback URL |
| {:state} | path | String | Y | A unique state value |
| {:scope} | path | String | The access scope; not used |
- Response
The Naver platform login page
The Naver 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 Naver API is as follows.
- Request
TXT
GET/POST https://nid.naver.com/oauth2.0/token?grant_type=authorization_code&client_id={:client_id}&client_secret={:client_secret}&code={:code}&state={:state}
| parameter | type | data | required | description |
|---|---|---|---|---|
| {:grant_type} | path | String | Y | The grant type. Fixed to authorization_code |
| {:client_id} | path | String | Y | The API key |
| {:client_secret} | path | String | Y | The secret key |
| {:code} | path | String | Y | The authorization code |
| {:state} | path | String | A unique state value |
- Response
JSON
{ "access_token": "AAAAQosjWDJieBiQZc3to9YQp6HDLvrmyKC+6+iZ3gq7qrkqf50ljZC+Lgoqrg", "refresh_token": "c8ceMEJisO4Se7uGisHoX0f5JEii7JnipglQipkOn5Zp3tyP7dHQoP0zNKHUq2gY", "token_type": "bearer", "expires_in": "3600" }
| parameter | data | description |
|---|---|---|
| access_token | String | The access token |
| refresh_token | String | The refresh token |
| token_type | String | The token type |
| expires_in | String | The expiration time (in seconds) |
The response of the service.getAccessToken() method gives us the OAuth2AccessToken object, which is the DTO for the JSON response above.
Since this too can be handled by the common method declared in AuthModule, there's no need to implement it separately.
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 Naver API implementing this is as follows.
- Request
TXT
GET/POST https://nid.naver.com/oauth2.0/token?grant_type=refresh_token&client_id={:client_id}&client_secret={:client_secret}&refresh_token=${:refresh_token}
| parameter | type | data | required | description |
|---|---|---|---|---|
| {:grant_type} | path | String | Y | The grant type. Fixed to refresh_token |
| {:client_id} | path | String | Y | The API key |
| {:client_secret} | path | String | Y | The secret key |
| {:refresh_token} | path | String | Y | The refresh token |
- Response
JSON
{ "access_token":"AAAAQjbRkysCNmMdQ7kmowPrjyRNIRYKG2iGHhbGawP0xfuYwjrE2WTI3p44SNepkFXME/NlxfamcJKPmUU4dSUhz+R2CmUqnN0lGuOcbEw6iexg", "token_type":"bearer", "expires_in":"3600" }
| parameter | data | description |
|---|---|---|
| access_token | String | The access token |
| token_type | String | The token type |
| expires_in | String | The 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 Naver API is as follows.
- Request
TXT
GET https://openapi.naver.com/v1/nid/me Authorization: Bearer {:access_token}
| parameter | type | data | required | description |
|---|---|---|---|---|
| {:access_token} | header | String | Y | The access token |
- Response
JSON
{ "resultcode": "00", "message": "success", "response": { "email": "openapi@naver.com", "nickname": "OpenAPI", "profile_image": "https://ssl.pstatic.net/static/pwe/address/nodata_33x33.gif", "age": "40-49", "gender": "F", "id": "32742776", "name": "오픈 API", "birthday": "10-01", "birthyear": "1900", "mobile": "010-0000-0000" } }
| parameter | data | description |
|---|---|---|
| resultcode | String | API call result code |
| message | String | The call result message |
| response.id | String | A unique identifier hash for the same person |
| response.nickname | String | The user's nickname (id*** if not set) |
| response.name | String | The user's name |
| response.email | String | The user's email address (based on the email in "My Info") |
| response.gender | String | Gender (F - female, M - male, U - unknown) |
| response.age | String | Age range |
| response.birthday | String | Birthday (MM-DD) |
| response.profile_image | String | URL of the user's profile picture |
| response.birthyear | String | Birth year |
| response.mobile | String | Mobile phone number |
The id is not the xxx@naver.com-style ID we normally think of, but a unique hash value assigned per account.
You can get the Naver ID via response.email, but it's limited. If you've changed [My Info - Contact Email] to a different email, that email will be shown instead of the Naver email. Officially, there's no way to reliably get the Naver email from the profile API.
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 Naver'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("response").get("email") == null ? "미동의" : node.get("response").get("email").textValue(); String name = node.get("response").get("name") == null ? "미동의" : node.get("response").get("name").textValue(); String profile_image = node.get("response").get("profile_image") == null ? "/oauth2/assets/images/logo.png" : node.get("response").get("profile_image").textValue(); return new UserInfoBean(email, name, profile_image, 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. Since Naver allows consent/refusal regardless of whether an item is required or optional, null handling must always be done for the data.
The first time you log in with a Naver 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 Naver and completely delete the information.
The Naver API is as follows.
- Request
TXT
GET/POST https://nid.naver.com/oauth2.0/token?grant_type=delete&client_id={:client_id}&client_secret={:client_secret}&access_token={:access_token}&service_provider=NAVER
| parameter | type | data | required | description |
|---|---|---|---|---|
| {:grant_type} | path | String | Y | The grant type. Fixed to delete |
| {:client_id} | path | String | Y | The API key |
| {:client_secret} | path | String | Y | The secret key |
| {:access_token} | path | String | Y | The access token |
| {:service_provider} | path | String | Y | The service provider. Fixed to NAVER |
- Response
JSON
{ "access_token": "c8ceMEjfnorlQwEisqemfpM1Wzw7aGp7JnipglQipkOn5Zp3tyP7dHQoP0zNKHUq2gY", "result": "success" }
| parameter | data | description |
|---|---|---|
| access_token | String | The access token that was deleted |
| result | String | The processing result; returns success if successful |
- Code
JAVA
@Override public boolean deleteInfo(String access) throws IOException, ExecutionException, InterruptedException { OAuthRequest oAuthRequest = new OAuthRequest(Verb.GET, getAccessTokenEndpoint()); oAuthRequest.addQuerystringParameter("client_id", API_KEY); oAuthRequest.addQuerystringParameter("client_secret", SECRET_KEY); oAuthRequest.addQuerystringParameter("access_token", access); oAuthRequest.addQuerystringParameter("grant_type", "delete"); oAuthRequest.addQuerystringParameter("service_provider", "NAVER"); service.signRequest(access, oAuthRequest); Response response = service.execute(oAuthRequest); return response.isSuccessful(); }
The implementation is simple. Using the OAuthRequest object, you can easily build the request. Since 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.
The API is as follows.
- Request
TXT
GET/POST https://nid.naver.com/oauth2.0/authorize?auth_type=reprompt&state=${:state}&response_type=code&client_id=${:client_id}&redirect_uri=${:redirect_uri}
| parameter | type | data | required | description |
|---|---|---|---|---|
| {:response_type} | path | String | Y | The response type. Fixed to code |
| {:client_id} | path | String | Y | The API key |
| {:redirect_uri} | path | String | Y | The Callback URL |
| {:state} | path | String | Y | A unique state value |
| {:auth_type} | path | String | The auth type. Fixed to reprompt |
- Response
The Naver platform's consent-to-provide-information page
On that page, the user can reselect whether to consent to providing information. After that, just like login, code and state are sent to the redirect URL. The behavior afterward is the same as login.
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.OAuthRequest; import com.github.scribejava.core.model.Response; import com.github.scribejava.core.model.Verb; import oauth.account.bean.ApiKeyBean; import oauth.account.bean.UserInfoBean; import java.io.IOException; import java.util.HashMap; import java.util.concurrent.ExecutionException; /** * Naver 인증 모듈 클래스 * * @author RWB * @since 2021.09.29 Wed 23:45:49 */ public class NaverAuthModule extends AuthModule { private static final String MODULE_NAME = "naver"; 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 NaverAuthModule INSTANCE = new NaverAuthModule(SERVICE_BUILDER); /** * 생성자 메서드 * * @param serviceBuilder: [ServiceBuilderOAuth20] API 서비스 빌더 */ private NaverAuthModule(ServiceBuilderOAuth20 serviceBuilder) { super(serviceBuilder); } /** * 인스턴스 반환 메서드 * * @return [NaverAuthModule] 인스턴스 */ public static NaverAuthModule getInstance() { return INSTANCE; } /** * 유저 정보 객체 반환 메서드 * * @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("response").get("email") == null ? "미동의" : node.get("response").get("email").textValue(); String name = node.get("response").get("name") == null ? "미동의" : node.get("response").get("name").textValue(); String profile_image = node.get("response").get("profile_image") == null ? "/oauth2/assets/images/logo.png" : node.get("response").get("profile_image").textValue(); return new UserInfoBean(email, name, profile_image, 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.GET, getAccessTokenEndpoint()); oAuthRequest.addQuerystringParameter("client_id", API_KEY); oAuthRequest.addQuerystringParameter("client_secret", SECRET_KEY); oAuthRequest.addQuerystringParameter("access_token", access); oAuthRequest.addQuerystringParameter("grant_type", "delete"); oAuthRequest.addQuerystringParameter("service_provider", "NAVER"); service.signRequest(access, oAuthRequest); Response response = service.execute(oAuthRequest); return response.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("auth_type", "reprompt"); return service.getAuthorizationUrl(params); } /** * 접근 토큰 요청 URL 반환 메서드 * * @return [String] 접근 토큰 요청 URL */ @Override public String getAccessTokenEndpoint() { return "https://nid.naver.com/oauth2.0/token"; } /** * 인증 API 요청 URL 반환 메서드 * * @return [String] 인증 API 요청 URL */ @Override protected String getAuthorizationBaseUrl() { return "https://nid.naver.com/oauth2.0/authorize"; } /** * 사용자 정보 요청 URL 반환 메서드 * * @return [String] 사용자 정보 요청 URL */ @Override protected String getUserInfoEndPoint() { return "https://openapi.naver.com/v1/nid/me"; } }
The full, organized code is as above.
With this, the implementation of the NAVER 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.
