blog.itcode.devblog.itcode.dev

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

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

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

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

시리즈 모아보기

Building an OAuth2.0 Authorization Server

5 / 11

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

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

After logging in, go to Google Cloud Platform.

You can apply for the OAuth service in Google Cloud Platform (GCP).


Click [APIs & Services] in the left sidebar to access the related menu.

Let's create a project that will manage the OAuth information. The name doesn't matter — pick anything you like.

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 [OAuth Consent Screen] menu in the left sidebar.

  • Internal - can only be used by a specified group. Since it's closed, the app review process can be skipped.
  • External - can be used by all users. Since it's open, the app review process is required.

Select the type you want. For this project, select [External].

Fill in the information about the application.

Fill in the required information and move on.

Specify the scope that the Access Token will have.

Since this project only plans to use profile information, select /auth/userinfo.email and /auth/userinfo.profile.

These two pieces of information are very basic, so they're shown as [Non-sensitive scopes]. You can add other scopes too, but keep in mind that selecting [Sensitive scopes] or [Restricted scopes] may require you to submit additional materials during app review.

Register the accounts that can use this OAuth API during the development stage. Since the application owner is an admin, there's no need to register them separately. If multiple accounts need to use it, for reasons such as collaboration, register them here.

You can review the information you've entered. This content can be modified at any time later through the same menu, following the same process.

Go to the [Credentials] menu in the left sidebar.

Select [OAuth Client ID] to create an API key.


Select the type of application. Here, select [Web application].

Enter whatever name you like.

  • Authorized JavaScript Origins - only for Implicit Grant. If you're calling the API directly from JavaScript with the Google API SDK, enter the URL from which the call is made.
  • Authorized Redirect URIs - only for Authorization Code Grant. Enter the URI to redirect to after authentication.

Since this project uses the Authorization Code Grant, select [Authorized Redirect URIs] and specify the URI to redirect to.

When calling up the platform login window, an error is shown if the URI isn't registered, so be sure to enter it accurately.

Once you finish saving, the API key is created.

You can click on the created API list to check the API.

You can renew the client secret via [Reset Secret] at the top.

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

JAVA

public class GoogleAuthModule extends AuthModule
{
	// Google authentication module
}

The basic form of the object is as above.

MethodMethod TypeDescriptionImplementation Needed?
getAuthorizationUrlabstractReturns the authentication URLY
getAccessTokenReturns the access token
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 Google module needs to implement is as above, the same as Naver.

Create a google.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 = "google";

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).defaultScope("https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile");

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

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

public static GoogleAuthModule 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
INSTANCEGoogleAuthModuleThe 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://oauth2.googleapis.com/token";
}

@Override
protected String getAuthorizationBaseUrl()
{
	return "https://accounts.google.com/o/oauth2/v2/auth?access_type=offline&prompt=consent";
}

@Override
protected String getUserInfoEndPoint()
{
	return "https://www.googleapis.com/oauth2/v2/userinfo";
}
  • 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 Google platform login URL.

First, let's look at the API.


  • Request

TXT

GET/POST https://accounts.google.com/o/oauth2/v2/auth?access_type=offline&prompt=consent&response_type=code&client_id=${:client_id}&redirect_uri={:redirect_uri}&scope={:scope}&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
{:scope}pathStringThe access scope. Enter the selected scopes separated by spaces
{:access_type}pathStringWhether this is a browser environment. Fixed to offline
{:prompt}pathStringPrompt mode. Fixed to consent

  • Response

The Google platform login page


The Google 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.


There's one difference here that's unique to Google compared to other platforms — the presence of access_type and prompt.

If you log in via Google using a normal URL, it only provides a Refresh Token on the very first login. In other words, you have to save the Refresh Token somewhere on that first login.

Since this information must not be lost, it's appropriate to store it in a DB rather than a cookie or local storage. However, this project doesn't store user information separately.

In this case, appending the above parameters to the login URL forces re-authentication every time, so a Refresh Token is provided on every login.


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 Google API is as follows.


  • Request

TXT

POST https://oauth2.googleapis.com/token?grant_type=authorization_code&client_id={:client_id}&client_secret={:client_secret}&code={:code}&state={:state}
parametertypedatarequireddescription
{:grant_type}pathStringYThe grant type. Fixed to authorization_code
{:client_id}pathStringYThe API key
{:client_secret}pathStringYThe secret key
{:code}pathStringYThe authorization code
{:state}pathStringA unique state value

  • Response

JSON

{
	"access_token": "1/fFAGRNJru1FTz70BzhT3Zg",
	"expires_in": 3920,
	"token_type": "Bearer",
	"scope": "https://www.googleapis.com/auth/drive.metadata.readonly",
	"refresh_token": "1//xEoDL4iW3cxlI7yDbSRFYNG01kVKM2C-259HOF2aQbI"
}
parameterdatadescription
access_tokenStringThe access token
refresh_tokenStringThe refresh token
token_typeStringThe token type
expires_inintThe expiration time (in seconds)
scopeStringThe access permissions

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 Google API implementing this is as follows.


  • Request

TXT

POST https://oauth2.googleapis.com/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": "1/fFAGRNJru1FTz70BzhT3Zg",
	"expires_in": 3920,
	"scope": "https://www.googleapis.com/auth/drive.metadata.readonly",
	"token_type": "Bearer"
}
parameterdatadescription
access_tokenStringThe access token
token_typeStringThe token type
expires_inStringThe expiration time (in seconds)
scopeStringThe access permissions

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 Google API is as follows.


  • Request

TXT

GET https://www.googleapis.com/oauth2/v3/userinfo
Authorization: Bearer {:access_token}
parametertypedatarequireddescription
{:access_token}headerStringYThe access token

  • Response

JSON

{
	"sub": "90234582532742",
	"name": "First Last",
	"given_name": "First",
	"family_name": "Last",
	"picture": "https://lh3.googleusercontent.com/a-/hash",
	"email": "example@gmail.com",
	"email_verified": true,
	"locale": "ko"
}
parameterdatadescription
subStringA unique identifier hash for the same person
nameStringFull name
given_nameStringGiven name
family_nameStringFamily name
pictureStringProfile picture URL
emailStringThe user's email address
email_verifiedbooleanWhether the email is verified
localeStringLocale

I couldn't find clearly provided docs for the Google Profile response, so this is written based on the response I got by requesting my own Access Token. In addition to the table above, more data may be sent depending on the scope, so keep that in mind.

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 Google'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("email") == null ? "미동의" : node.get("email").textValue();
	String name = node.get("name") == null ? "미동의" : node.get("name").textValue();
	String picture = node.get("picture") == null ? "/oauth2/assets/images/logo.png" : node.get("picture").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 Google 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 Google and completely delete the information.

The Google API is as follows.


  • Request

TXT

POST https://oauth2.googleapis.com/revoke?token={:token}
parametertypedatarequireddescription
{:token}pathStringYThe access token

  • Response

JSON

{}

  • Code

JAVA

@Override
public boolean deleteInfo(String access) throws IOException, ExecutionException, InterruptedException
{
	OAuthRequest oAuthRequest = new OAuthRequest(Verb.POST, "https://oauth2.googleapis.com/revoke");
	oAuthRequest.addBodyParameter("token", access);
	
	service.signRequest(access, oAuthRequest);
	
	return service.execute(oAuthRequest).isSuccessful();
}

The implementation is simple. Using the OAuthRequest object, you can easily build the request. Since the response body itself doesn't matter, and for Google a response code of 200 is equivalent to a 204 with no content at all, we just use response.isSuccessful() to determine whether the response is normal and return that as a boolean.

For Google, profile information doesn't require separate consent. So this feature is excluded from the implementation.


  • Code

JAVA

@Override
public String getUpdateAuthorizationUrl(String state)
{
	return null;
}

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.OAuthRequest;
import com.github.scribejava.core.model.Verb;
import oauth.account.bean.ApiKeyBean;
import oauth.account.bean.UserInfoBean;

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

/**
 * Google 인증 모듈 클래스
 *
 * @author RWB
 * @since 2021.09.29 Wed 23:45:27
 */
public class GoogleAuthModule extends AuthModule
{
	private static final String MODULE_NAME = "google";
	
	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).defaultScope("https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile");
	
	private static final GoogleAuthModule INSTANCE = new GoogleAuthModule(SERVICE_BUILDER);
	
	/**
	 * 생성자 메서드
	 *
	 * @param serviceBuilder: [ServiceBuilderOAuth20] API 서비스 빌더
	 */
	private GoogleAuthModule(ServiceBuilderOAuth20 serviceBuilder)
	{
		super(serviceBuilder);
	}
	
	/**
	 * 인스턴스 반환 메서드
	 *
	 * @return [GoogleAuthModule] 인스턴스
	 */
	public static GoogleAuthModule 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("email") == null ? "미동의" : node.get("email").textValue();
		String name = node.get("name") == null ? "미동의" : node.get("name").textValue();
		String picture = node.get("picture") == null ? "/oauth2/assets/images/logo.png" : node.get("picture").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://oauth2.googleapis.com/revoke");
		oAuthRequest.addBodyParameter("token", access);
		
		service.signRequest(access, oAuthRequest);
		
		return service.execute(oAuthRequest).isSuccessful();
	}
	
	/**
	 * 정보 제공 동의 갱신 URL 반환 메서드
	 *
	 * @param state: [String] 고유 상태값
	 *
	 * @return [String] 정보 제공 동의 갱신 URL
	 */
	@Override
	public String getUpdateAuthorizationUrl(String state)
	{
		return null;
	}
	
	/**
	 * 접근 토큰 요청 URL 반환 메서드
	 *
	 * @return [String] 접근 토큰 요청 URL
	 */
	@Override
	public String getAccessTokenEndpoint()
	{
		return "https://oauth2.googleapis.com/token";
	}
	
	/**
	 * 인증 API 요청 URL 반환 메서드
	 *
	 * @return [String] 인증 API 요청 URL
	 */
	@Override
	protected String getAuthorizationBaseUrl()
	{
		return "https://accounts.google.com/o/oauth2/v2/auth?access_type=offline&prompt=consent";
	}
	
	/**
	 * 사용자 정보 요청 URL 반환 메서드
	 *
	 * @return [String] 사용자 정보 요청 URL
	 */
	@Override
	protected String getUserInfoEndPoint()
	{
		return "https://www.googleapis.com/oauth2/v3/userinfo";
	}
}

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.

Google was a bit tricky to develop against. Since Google offers so many services, the documentation is massive, and there wasn't a document that explained things simply and concisely, so I had to search around in a lot of different places.

# 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