[OAuth2.0] Building an OAuth2.0 Authorization Server with ScribeJAVA - 8. Implementing the Processes
[OAuth2.0] Building an OAuth2.0 Authorization Server with ScribeJAVA - 8. Implementing the Processes
From chapter 4 through chapter 7, we implemented the authentication module for each platform. In this chapter, we implement the processes, the layer that uses these modules.
Processes are organized and managed by HTTP method.
Since there's nothing beyond account-related operations, the only top-level category is account.
The HTTP methods needed for the work are GET, POST, PUT, and DELETE, so we split them up as below.
- AccountGetProcess - the account GET process class
- AccountPostProcess - the account POST process class
- AccountPutProcess - the account PUT process class
- AccountDeleteProcess - the account DELETE process class
Logic used for the GET method is organized so it belongs in AccountGetProcess, and so on.
To apply common logic across multiple processes, let's implement a Process abstract class that all process objects will extend.
JAVA
package global.module; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import oauth.account.module.AuthModule; import oauth.account.module.GithubAuthModule; import oauth.account.module.GoogleAuthModule; import oauth.account.module.KakaoAuthModule; import oauth.account.module.NaverAuthModule; /** * 프로세스 추상 클래스 * * @author RWB * @since 2021.09.30 Thu 01:14:25 */ abstract public class Process { protected HttpServletRequest request; protected HttpServletResponse response; /** * 생성자 메서드 * * @param request: [HttpServletRequest] HttpServletResponse 객체 * @param response: [HttpServletResponse] HttpServletResponse 객체 */ protected Process(HttpServletRequest request, HttpServletResponse response) { this.request = request; this.response = response; } /** * 인증 모듈 반환 메서드 * * @param platform: [String] 플랫폼 * * @return [AuthModule] AuthModule 객체 * * @throws NullPointerException 유효하지 않은 플랫폼 */ protected AuthModule getAuthModule(String platform) throws NullPointerException { return switch (platform) { case "naver" -> NaverAuthModule.getInstance(); case "google" -> GoogleAuthModule.getInstance(); case "kakao" -> KakaoAuthModule.getInstance(); case "github" -> GithubAuthModule.getInstance(); default -> throw new NullPointerException(Util.builder("'", platform, "' is invalid platform")); }; } }
To make it easy to access the servlet objects HttpServletRequest and HttpServletResponse, each local variable is declared with the protected access modifier.
When using the constructor, it's required to always pass HttpServletRequest and HttpServletResponse as arguments.
This means every subclass process that extends Process is required to accept the servlet objects as arguments, and can access the servlet objects via request and response inside the process.
getAuthModule is a method that returns the corresponding instance based on each platform's name. Since the authentication module is used heavily by the processes, it's declared in Process so that every process class that extends it can access this method.
With this structure, the same process can call the AuthModule object for a given platform and use the methods declared for that particular platform.
Let's implement the process class that groups together the operations corresponding to GET among the account processes.
- Method to return the authorization URL response
- Method to return the user info response
The operations corresponding to GET are the two methods above. They mostly consist of operations that simply fetch data.
This method returns the authorization URL for the platform login.
It obtains the URL via AuthModule's getAuthorizationUrl method, builds a response object with that content, and returns it.
JAVA
public Response getAuthorizationUrlResponse(String platform) { Response response; ResponseBean<String> responseBean = new ResponseBean<>(); // 인증 URL 응답 생성 시도 try { String state = UUID.randomUUID().toString(); request.getSession().setAttribute("state", state); AuthModule authModule = getAuthModule(platform); responseBean.setFlag(true); responseBean.setTitle("success"); responseBean.setMessage(Util.builder(platform, " authrorization url response success")); responseBean.setBody(authModule.getAuthorizationUrl(state)); response = Response.ok(responseBean, MediaType.APPLICATION_JSON).build(); } // 예외 catch (Exception e) { e.printStackTrace(); responseBean.setFlag(false); responseBean.setTitle(e.getClass().getSimpleName()); responseBean.setMessage(e.getMessage()); responseBean.setBody(null); response = Response.status(Response.Status.BAD_REQUEST).entity(responseBean).type(MediaType.APPLICATION_JSON).build(); } return response; }
To verify it's the same session, a state value is generated when this process runs and passed to getAuthorizationUrl. The state we pass will appear as a URL parameter in the URL returned by that method.
We also register that same state as a session attribute.
Since a platform login goes through multiple windows, request hijacking is fairly easy. Session information can easily get corrupted in this process, so state lets us verify that the entire login flow is happening within the same session.
If the state in the URL doesn't match the state in the session, or if there's no session information at all, we can conclude this isn't a legitimate login flow.
This session value is later used when receiving the Access Token and completing the login operation.
This method retrieves the user response via the Access Token.
It obtains a UserInfoBean object via AuthModule's getUserInfoBean method, builds a response object with that content, and returns it.
JAVA
public Response getUserInfoResponse(String accessCookie) { Response response; ResponseBean<UserInfoBean> responseBean = new ResponseBean<>(); // 사용자 정보 응답 생성 시도 try { Jws<Claims> jws = JwtModule.openJwt(accessCookie); String accessToken = jws.getBody().get("access", String.class); String platform = jws.getBody().get("platform", String.class); AuthModule authModule = getAuthModule(platform); com.github.scribejava.core.model.Response userInfoResponse = authModule.getUserInfo(accessToken); // 응답이 정상적이지 않을 경우 if (userInfoResponse.getCode() != 200) { throw new OAuthResponseException(userInfoResponse); } responseBean.setFlag(true); responseBean.setTitle("success"); responseBean.setMessage("user info response success"); responseBean.setBody(authModule.getUserInfoBean(userInfoResponse.getBody())); response = Response.ok(responseBean, MediaType.APPLICATION_JSON).build(); } // 예외 catch (Exception e) { e.printStackTrace(); responseBean.setFlag(false); responseBean.setTitle(e.getClass().getSimpleName()); responseBean.setMessage(e.getMessage()); responseBean.setBody(null); response = Response.status(Response.Status.BAD_REQUEST).entity(responseBean).type(MediaType.APPLICATION_JSON).build(); } return response; }
As will be explained later, at login time, the Access Token and Refresh Token are each built into a JWT together with the platform, and stored as the access and refresh cookies.
Since each JWT cookie contains the platform info, having just the access cookie is enough to find both the Access Token and its platform.
JAVA
package oauth.account.process; import com.github.scribejava.core.model.OAuthResponseException; import global.bean.ResponseBean; import global.module.JwtModule; import global.module.Process; import global.module.Util; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jws; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; import oauth.account.bean.UserInfoBean; import oauth.account.module.AuthModule; import java.util.UUID; /** * 계정 GET 프로세스 클래스 * * @author RWB * @since 2021.09.30 Thu 21:00:48 */ public class AccountGetProcess extends Process { /** * 생성자 메서드 * * @param request: [HttpServletRequest] HttpServletRequest 객체 * @param response: [HttpServletResponse] HttpServletResponse 객체 */ public AccountGetProcess(HttpServletRequest request, HttpServletResponse response) { super(request, response); } /** * 인증 URL 응답 반환 메서드 * * @param platform: [String] 플랫폼 * * @return [Response] 응답 객체 */ public Response getAuthorizationUrlResponse(String platform) { Response response; ResponseBean<String> responseBean = new ResponseBean<>(); // 인증 URL 응답 생성 시도 try { String state = UUID.randomUUID().toString(); request.getSession().setAttribute("state", state); AuthModule authModule = getAuthModule(platform); responseBean.setFlag(true); responseBean.setTitle("success"); responseBean.setMessage(Util.builder(platform, " authrorization url response success")); responseBean.setBody(authModule.getAuthorizationUrl(state)); response = Response.ok(responseBean, MediaType.APPLICATION_JSON).build(); } // 예외 catch (Exception e) { e.printStackTrace(); responseBean.setFlag(false); responseBean.setTitle(e.getClass().getSimpleName()); responseBean.setMessage(e.getMessage()); responseBean.setBody(null); response = Response.status(Response.Status.BAD_REQUEST).entity(responseBean).type(MediaType.APPLICATION_JSON).build(); } return response; } /** * 사용자 정보 응답 반환 메서드 * * @param accessCookie: [String] 접근 토큰 쿠키 * * @return [Response] 응답 객체 */ public Response getUserInfoResponse(String accessCookie) { Response response; ResponseBean<UserInfoBean> responseBean = new ResponseBean<>(); // 사용자 정보 응답 생성 시도 try { Jws<Claims> jws = JwtModule.openJwt(accessCookie); String accessToken = jws.getBody().get("access", String.class); String platform = jws.getBody().get("platform", String.class); AuthModule authModule = getAuthModule(platform); com.github.scribejava.core.model.Response userInfoResponse = authModule.getUserInfo(accessToken); // 응답이 정상적이지 않을 경우 if (userInfoResponse.getCode() != 200) { throw new OAuthResponseException(userInfoResponse); } responseBean.setFlag(true); responseBean.setTitle("success"); responseBean.setMessage("user info response success"); responseBean.setBody(authModule.getUserInfoBean(userInfoResponse.getBody())); response = Response.ok(responseBean, MediaType.APPLICATION_JSON).build(); } // 예외 catch (Exception e) { e.printStackTrace(); responseBean.setFlag(false); responseBean.setTitle(e.getClass().getSimpleName()); responseBean.setMessage(e.getMessage()); responseBean.setBody(null); response = Response.status(Response.Status.BAD_REQUEST).entity(responseBean).type(MediaType.APPLICATION_JSON).build(); } return response; } }
Let's implement the process class that groups together the operations corresponding to POST among the account processes.
- Method to return the login response
- Method to return the auto-login response
- Method to return the logout response
The operations corresponding to POST are the methods above. They mostly consist of login/logout operations.
This method performs login by exchanging the code issued after the platform login for an Access Token.
It obtains an OAuth2AccessToken object via AuthModule's getAccessToken method and extracts the Access Token and Refresh Token.
These tokens are built into JWT cookies to complete the login process.
JAVA
public Response postLoginResponse(String platform, String code, String state) { Response response; ResponseBean<String> responseBean = new ResponseBean<>(); HttpSession session = request.getSession(); // 로그인 응답 생성 시도 try { Object sessionState = Objects.requireNonNull(session.getAttribute("state")); // 고유 상태값이 일치하지 않을 경우 if (!state.equals(sessionState)) { throw new BadAttributeValueExpException("state is mismatched"); } AuthModule authModule = getAuthModule(platform); OAuth2AccessToken oAuth2AccessToken = authModule.getAccessToken(code); String accessToken = oAuth2AccessToken.getAccessToken(); String refreshToken = oAuth2AccessToken.getRefreshToken(); HashMap<String, Object> accessMap = new HashMap<>(); accessMap.put("access", accessToken); accessMap.put("platform", platform); HashMap<String, Object> refreshMap = new HashMap<>(); refreshMap.put("refresh", refreshToken); refreshMap.put("platform", platform); String accessJwt = JwtModule.generateJwt(state, accessMap); String refreshJwt = JwtModule.generateJwt(state, refreshMap); NewCookie accessCookie = new NewCookie("access", accessJwt, "/oauth2", ".itcode.dev", "access token", -1, true, true); NewCookie refreshCookie = new NewCookie("refresh", refreshJwt, "/oauth2", ".itcode.dev", "refresh token", refreshToken == null ? 0 : 86400 * 7 + 3600 * 9, true, true); responseBean.setFlag(true); responseBean.setTitle("success"); responseBean.setMessage("authorized success"); responseBean.setBody(null); response = Response.ok(responseBean, MediaType.APPLICATION_JSON).cookie(accessCookie, refreshCookie).build(); } // 예외 catch (Exception e) { e.printStackTrace(); responseBean.setFlag(false); responseBean.setTitle(e.getClass().getSimpleName()); responseBean.setMessage(e.getMessage()); responseBean.setBody(null); response = Response.status(Response.Status.BAD_REQUEST).entity(responseBean).type(MediaType.APPLICATION_JSON).build(); } // 시도 후 finally { session.invalidate(); } return response; }
During the getAuthorizationUrlResponse operation in AccountGetProcess, we stored state in a session attribute — here, that session value is used to perform verification.
We extract the state received as an argument via the URL and the state from the session and compare them, throwing an exception if they don't match. This is a security measure that prevents an attacker from hijacking the URL midway and sending a request with a completely different code.
Once the Access Token and Refresh Token are received, they're built into JWT cookies.
- Access Token JWT
JSON
{ "iss": "oauth2", "sub": "auth", "aud": "c9159786-40bf-4cf2-8c93-f683d1070137", "access": "{ACCESS_TOKEN}", "platform": "naver", "exp": 1634986011, "nbf": 1634982411, "iat": 1634982411, "jti": "c9159786-40bf-4cf2-8c93-f683d1070137" }
- Refresh Token JWT
JSON
{ "iss": "oauth2", "sub": "auth", "aud": "c9159786-40bf-4cf2-8c93-f683d1070137", "refresh": "{REFRESH_TOKEN}", "platform": "naver", "exp": 1634986011, "nbf": 1634982411, "iat": 1634982411, "jti": "c9159786-40bf-4cf2-8c93-f683d1070137" }
The content of the JWT is as above. The cookie is created carrying this JWT. The access cookie is created as a session cookie so that it's immediately destroyed when the browser closes, while the refresh cookie is given some retention period so it can be used again later.
The cookie domain is set to .itcode.dev, because the Frontend and Backend run in completely different environments.
- Frontend - project.itcode.dev
- Backend - api.itcode.dev
Due to the browser's security policy, cookies can't be created for a different domain. So it's set to .itcode.dev so it applies to all subdomains.
If the domain isn't specified, the cookie is automatically issued targeting api.itcode.dev. As a result, the project.itcode.dev domain service wouldn't be able to access the cookie.
If a user has logged in before and already has access and refresh cookies, this method uses them to perform auto-login.
JAVA
public Response postAutoLoginResponse(String accessCookie, String refreshCookie) { Response response; ResponseBean<String> responseBean = new ResponseBean<>(); // 자동 로그인 시도 try { // 접근 토큰 쿠키가 있을 경우 if (accessCookie != null) { responseBean.setFlag(true); responseBean.setTitle("success"); responseBean.setMessage("auto authorized success"); responseBean.setBody(null); response = Response.ok(responseBean, MediaType.APPLICATION_JSON).build(); } // 리프레쉬 토큰 쿠키가 없을 경우 else if (refreshCookie == null) { responseBean.setFlag(false); responseBean.setTitle("fail"); responseBean.setMessage("refresh token is empty"); responseBean.setBody(null); response = Response.ok(responseBean, MediaType.APPLICATION_JSON).build(); } // 리프레쉬 토큰 쿠키가 있을 경우 else { Jws<Claims> refreshJws = JwtModule.openJwt(refreshCookie); String refreshToken = refreshJws.getBody().get("refresh", String.class); String platform = refreshJws.getBody().get("platform", String.class); AuthModule authModule = getAuthModule(platform); OAuth2AccessToken oAuth2AccessToken = authModule.getRefreshAccessToken(refreshToken); String accessToken = oAuth2AccessToken.getAccessToken(); HashMap<String, Object> accessMap = new HashMap<>(); accessMap.put("access", accessToken); accessMap.put("platform", platform); HashMap<String, Object> refreshMap = new HashMap<>(); refreshMap.put("refresh", refreshToken); refreshMap.put("platform", platform); String uuid = UUID.randomUUID().toString(); String accessJwt = JwtModule.generateJwt(uuid, accessMap); String refreshJwt = JwtModule.generateJwt(uuid, refreshMap); NewCookie newAccessCookie = new NewCookie("access", accessJwt, "/oauth2", ".itcode.dev", "access token", -1, true, true); NewCookie newRefreshCookie = new NewCookie("refresh", refreshJwt, "/oauth2", ".itcode.dev", "refresh token", 86400 * 7 + 3600 * 9, true, true); responseBean.setFlag(true); responseBean.setTitle("success"); responseBean.setMessage("auto authorized success"); responseBean.setBody(null); response = Response.ok(responseBean, MediaType.APPLICATION_JSON).cookie(newAccessCookie, newRefreshCookie).build(); } } // 예외 catch (Exception e) { e.printStackTrace(); responseBean.setFlag(false); responseBean.setTitle(e.getClass().getSimpleName()); responseBean.setMessage(e.getMessage()); responseBean.setBody(null); NewCookie newAccessCookie = new NewCookie("access", null, "/oauth2", ".itcode.dev", "access token", 0, true, true); NewCookie newRefreshCookie = new NewCookie("refresh", null, "/oauth2", ".itcode.dev", "refresh token", 0, true, true); response = Response.status(Response.Status.BAD_REQUEST).entity(responseBean).type(MediaType.APPLICATION_JSON).cookie(newAccessCookie, newRefreshCookie).build(); } return response; }
If the access cookie is already present, since we already have valid authentication info, no further action is taken and we simply move on.
If there's no access cookie but only a refresh cookie exists, this uses it to reissue the Access Token and refresh the authentication info, performing login automatically.
The login logic itself is the same as the regular login method — only the Access Token is refreshed using the Refresh Token.
A method that performs logout by removing the authentication info.
JAVA
public Response postLogoutResponse() { Response response; ResponseBean<String> responseBean = new ResponseBean<>(); // 로그아웃 응답 생성 시도 try { NewCookie accessCookie = new NewCookie("access", null, "/oauth2", ".itcode.dev", "access token", 0, true, true); NewCookie refreshCookie = new NewCookie("refresh", null, "/oauth2", ".itcode.dev", "refresh token", 0, true, true); responseBean.setFlag(true); responseBean.setTitle("success"); responseBean.setMessage("logout success"); responseBean.setBody(null); response = Response.ok(responseBean, MediaType.APPLICATION_JSON).cookie(accessCookie, refreshCookie).build(); } // 예외 catch (Exception e) { e.printStackTrace(); responseBean.setFlag(false); responseBean.setTitle(e.getClass().getSimpleName()); responseBean.setMessage(e.getMessage()); responseBean.setBody(null); response = Response.status(Response.Status.BAD_REQUEST).entity(responseBean).type(MediaType.APPLICATION_JSON).build(); } return response; }
The authentication info is cookie-based. Since the HttpOnly option is enabled for security when the server creates the cookies, JavaScript cannot touch the access and refresh cookies.
The server overwrites the cookie expiration time with 0 to remove the authentication cookies.
JAVA
package oauth.account.process; import com.github.scribejava.core.model.OAuth2AccessToken; import global.bean.ResponseBean; import global.module.JwtModule; import global.module.Process; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jws; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpSession; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.NewCookie; import jakarta.ws.rs.core.Response; import oauth.account.module.AuthModule; import javax.management.BadAttributeValueExpException; import java.util.HashMap; import java.util.Objects; import java.util.UUID; /** * 계정 POST 프로세스 클래스 * * @author RWB * @since 2021.10.02 Sat 00:53:52 */ public class AccountPostProcess extends Process { /** * 생성자 메서드 * * @param request: [HttpServletRequest] HttpServletRequest 객체 * @param response: [HttpServletResponse] HttpServletResponse 객체 */ public AccountPostProcess(HttpServletRequest request, HttpServletResponse response) { super(request, response); } /** * 로그인 응답 반환 메서드 * * @param platform: [String] 플랫폼 * @param code: [String] 인증 코드 * @param state: [String] 고유 상태값 * * @return [Response] 응답 객체 */ public Response postLoginResponse(String platform, String code, String state) { Response response; ResponseBean<String> responseBean = new ResponseBean<>(); HttpSession session = request.getSession(); // 로그인 응답 생성 시도 try { Object sessionState = Objects.requireNonNull(session.getAttribute("state")); // 고유 상태값이 일치하지 않을 경우 if (!state.equals(sessionState)) { throw new BadAttributeValueExpException("state is mismatched"); } AuthModule authModule = getAuthModule(platform); OAuth2AccessToken oAuth2AccessToken = authModule.getAccessToken(code); String accessToken = oAuth2AccessToken.getAccessToken(); String refreshToken = oAuth2AccessToken.getRefreshToken(); HashMap<String, Object> accessMap = new HashMap<>(); accessMap.put("access", accessToken); accessMap.put("platform", platform); HashMap<String, Object> refreshMap = new HashMap<>(); refreshMap.put("refresh", refreshToken); refreshMap.put("platform", platform); String accessJwt = JwtModule.generateJwt(state, accessMap); String refreshJwt = JwtModule.generateJwt(state, refreshMap); NewCookie accessCookie = new NewCookie("access", accessJwt, "/oauth2", ".itcode.dev", "access token", -1, true, true); NewCookie refreshCookie = new NewCookie("refresh", refreshJwt, "/oauth2", ".itcode.dev", "refresh token", refreshToken == null ? 0 : 86400 * 7 + 3600 * 9, true, true); responseBean.setFlag(true); responseBean.setTitle("success"); responseBean.setMessage("authorized success"); responseBean.setBody(null); response = Response.ok(responseBean, MediaType.APPLICATION_JSON).cookie(accessCookie, refreshCookie).build(); } // 예외 catch (Exception e) { e.printStackTrace(); responseBean.setFlag(false); responseBean.setTitle(e.getClass().getSimpleName()); responseBean.setMessage(e.getMessage()); responseBean.setBody(null); response = Response.status(Response.Status.BAD_REQUEST).entity(responseBean).type(MediaType.APPLICATION_JSON).build(); } // 시도 후 finally { session.invalidate(); } return response; } /** * 자동 로그인 응답 반환 메서드 * * @param accessCookie: [String] 접근 토큰 쿠키 * @param refreshCookie: [String] 리프레쉬 토큰 쿠키 * * @return [Response] 응답 객체 */ public Response postAutoLoginResponse(String accessCookie, String refreshCookie) { Response response; ResponseBean<String> responseBean = new ResponseBean<>(); // 자동 로그인 시도 try { // 접근 토큰 쿠키가 있을 경우 if (accessCookie != null) { responseBean.setFlag(true); responseBean.setTitle("success"); responseBean.setMessage("auto authorized success"); responseBean.setBody(null); response = Response.ok(responseBean, MediaType.APPLICATION_JSON).build(); } // 리프레쉬 토큰 쿠키가 없을 경우 else if (refreshCookie == null) { responseBean.setFlag(false); responseBean.setTitle("fail"); responseBean.setMessage("refresh token is empty"); responseBean.setBody(null); response = Response.ok(responseBean, MediaType.APPLICATION_JSON).build(); } // 리프레쉬 토큰 쿠키가 있을 경우 else { Jws<Claims> refreshJws = JwtModule.openJwt(refreshCookie); String refreshToken = refreshJws.getBody().get("refresh", String.class); String platform = refreshJws.getBody().get("platform", String.class); AuthModule authModule = getAuthModule(platform); OAuth2AccessToken oAuth2AccessToken = authModule.getRefreshAccessToken(refreshToken); String accessToken = oAuth2AccessToken.getAccessToken(); HashMap<String, Object> accessMap = new HashMap<>(); accessMap.put("access", accessToken); accessMap.put("platform", platform); HashMap<String, Object> refreshMap = new HashMap<>(); refreshMap.put("refresh", refreshToken); refreshMap.put("platform", platform); String uuid = UUID.randomUUID().toString(); String accessJwt = JwtModule.generateJwt(uuid, accessMap); String refreshJwt = JwtModule.generateJwt(uuid, refreshMap); NewCookie newAccessCookie = new NewCookie("access", accessJwt, "/oauth2", ".itcode.dev", "access token", -1, true, true); NewCookie newRefreshCookie = new NewCookie("refresh", refreshJwt, "/oauth2", ".itcode.dev", "refresh token", 86400 * 7 + 3600 * 9, true, true); responseBean.setFlag(true); responseBean.setTitle("success"); responseBean.setMessage("auto authorized success"); responseBean.setBody(null); response = Response.ok(responseBean, MediaType.APPLICATION_JSON).cookie(newAccessCookie, newRefreshCookie).build(); } } // 예외 catch (Exception e) { e.printStackTrace(); responseBean.setFlag(false); responseBean.setTitle(e.getClass().getSimpleName()); responseBean.setMessage(e.getMessage()); responseBean.setBody(null); NewCookie newAccessCookie = new NewCookie("access", null, "/oauth2", ".itcode.dev", "access token", 0, true, true); NewCookie newRefreshCookie = new NewCookie("refresh", null, "/oauth2", ".itcode.dev", "refresh token", 0, true, true); response = Response.status(Response.Status.BAD_REQUEST).entity(responseBean).type(MediaType.APPLICATION_JSON).cookie(newAccessCookie, newRefreshCookie).build(); } return response; } /** * 로그아웃 응답 반환 메서드 * * @return [Response] 응답 객체 */ public Response postLogoutResponse() { Response response; ResponseBean<String> responseBean = new ResponseBean<>(); // 로그아웃 응답 생성 시도 try { NewCookie accessCookie = new NewCookie("access", null, "/oauth2", ".itcode.dev", "access token", 0, true, true); NewCookie refreshCookie = new NewCookie("refresh", null, "/oauth2", ".itcode.dev", "refresh token", 0, true, true); responseBean.setFlag(true); responseBean.setTitle("success"); responseBean.setMessage("logout success"); responseBean.setBody(null); response = Response.ok(responseBean, MediaType.APPLICATION_JSON).cookie(accessCookie, refreshCookie).build(); } // 예외 catch (Exception e) { e.printStackTrace(); responseBean.setFlag(false); responseBean.setTitle(e.getClass().getSimpleName()); responseBean.setMessage(e.getMessage()); responseBean.setBody(null); response = Response.status(Response.Status.BAD_REQUEST).entity(responseBean).type(MediaType.APPLICATION_JSON).build(); } return response; } }
Let's implement the process that groups together the operations corresponding to PUT among the account processes.
- Method to return the response for renewing the consent-to-provide-information URL
There's a single method corresponding to PUT. It consists of operations for modifying data.
This method returns the URL that newly renews the consent to provide information.
JAVA
public Response putUpdateAuthorizationUrl(String accessCookie) { Response response; ResponseBean<String> responseBean = new ResponseBean<>(); // 정보 제공 동의 갱신 URL 응답 생성 시도 try { String state = UUID.randomUUID().toString(); Jws<Claims> jws = JwtModule.openJwt(accessCookie); String platform = jws.getBody().get("platform", String.class); AuthModule authModule = getAuthModule(platform); String url = authModule.getUpdateAuthorizationUrl(state); // URL이 null일 경우 if (url == null) { responseBean.setFlag(false); responseBean.setTitle("skipped"); responseBean.setMessage(Util.builder(platform, " doesn't need that service")); responseBean.setBody(null); } // URL이 유효할 경우 else { request.getSession().setAttribute("state", state); responseBean.setFlag(true); responseBean.setTitle("success"); responseBean.setMessage(Util.builder(platform, " reauthrorization url response success")); responseBean.setBody(url); } response = Response.ok(responseBean, MediaType.APPLICATION_JSON).build(); } // 예외 catch (Exception e) { e.printStackTrace(); responseBean.setFlag(false); responseBean.setTitle(e.getClass().getSimpleName()); responseBean.setMessage(e.getMessage()); responseBean.setBody(null); response = Response.status(Response.Status.BAD_REQUEST).entity(responseBean).type(MediaType.APPLICATION_JSON).build(); } return response; }
The first time you log in, you go through a process of consenting to or refusing the information the service requires. Afterward, when requesting user info, only the information the user has consented to is provided, based on their consent record.
If, while the service is running, the required information changes and additional information is needed, we need to renew the consent to provide information.
The service redirects to the returned URL, renews the consent, and then a code reflecting the updated information is returned. The process after that follows the same steps as login.
In other words, renewing consent to provide information is essentially the same as performing login again with newly updated information.
JAVA
package oauth.account.process; import global.bean.ResponseBean; import global.module.JwtModule; import global.module.Process; import global.module.Util; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jws; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; import oauth.account.module.AuthModule; import java.util.UUID; /** * 계정 PUT 프로세스 클래스 * * @author RWB * @since 2021.10.19 Tue 21:56:32 */ public class AccountPutProcess extends Process { /** * 생성자 메서드 * * @param request: [HttpServletRequest] HttpServletRequest 객체 * @param response: [HttpServletResponse] HttpServletResponse 객체 */ public AccountPutProcess(HttpServletRequest request, HttpServletResponse response) { super(request, response); } /** * 정보 제공 동의 갱신 URL 응답 반환 메서드 * * @param accessCookie: [String] 접근 토큰 쿠키 * * @return [Response] 응답 객체 */ public Response putUpdateAuthorizationUrl(String accessCookie) { Response response; ResponseBean<String> responseBean = new ResponseBean<>(); // 정보 제공 동의 갱신 URL 응답 생성 시도 try { String state = UUID.randomUUID().toString(); Jws<Claims> jws = JwtModule.openJwt(accessCookie); String platform = jws.getBody().get("platform", String.class); AuthModule authModule = getAuthModule(platform); String url = authModule.getUpdateAuthorizationUrl(state); // URL이 null일 경우 if (url == null) { responseBean.setFlag(false); responseBean.setTitle("skipped"); responseBean.setMessage(Util.builder(platform, " doesn't need that service")); responseBean.setBody(null); } // URL이 유효할 경우 else { request.getSession().setAttribute("state", state); responseBean.setFlag(true); responseBean.setTitle("success"); responseBean.setMessage(Util.builder(platform, " reauthrorization url response success")); responseBean.setBody(url); } response = Response.ok(responseBean, MediaType.APPLICATION_JSON).build(); } // 예외 catch (Exception e) { e.printStackTrace(); responseBean.setFlag(false); responseBean.setTitle(e.getClass().getSimpleName()); responseBean.setMessage(e.getMessage()); responseBean.setBody(null); response = Response.status(Response.Status.BAD_REQUEST).entity(responseBean).type(MediaType.APPLICATION_JSON).build(); } return response; } }
Let's implement the process that groups together the operations corresponding to DELETE among the account processes.
- Method to return the unlink response
There's a single method corresponding to DELETE. It mostly consists of operations for deleting data.
This method completely unlinks from the platform and performs logout.
JAVA
public Response deleteInfoResponse(String accessCookie) { Response response; ResponseBean<String> responseBean = new ResponseBean<>(); // 연동 해제 응답 생성 시도 try { Jws<Claims> jws = JwtModule.openJwt(accessCookie); String accessToken = jws.getBody().get("access", String.class); String platform = jws.getBody().get("platform", String.class); AuthModule authModule = getAuthModule(platform); // 연동 해제에 성공할 경우 if (authModule.deleteInfo(accessToken)) { response = new AccountPostProcess(request, this.response).postLogoutResponse(); } // 아닐 경우 else { throw new RequestAuthenticationException("revoke fail"); } } // 예외 catch (Exception e) { e.printStackTrace(); responseBean.setFlag(false); responseBean.setTitle(e.getClass().getSimpleName()); responseBean.setMessage(e.getMessage()); responseBean.setBody(null); response = Response.status(Response.Status.BAD_REQUEST).entity(responseBean).type(MediaType.APPLICATION_JSON).build(); } return response; }
Once you unlink from the platform, the previously issued Access Token and Refresh Token expire and can no longer function.
This is usually done as part of account withdrawal, but since this project doesn't have a separate sign-up process, it simply logs the user out automatically.
The next time the user logs in, they'll go through the same process as logging in for the first time.
JAVA
package oauth.account.process; import global.bean.ResponseBean; import global.module.JwtModule; import global.module.Process; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jws; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; import oauth.account.module.AuthModule; import org.glassfish.jersey.client.authentication.RequestAuthenticationException; /** * 계정 DELETE 프로세스 클래스 * * @author RWB * @since 2021.10.02 Sat 00:53:52 */ public class AccountDeleteProcess extends Process { /** * 생성자 메서드 * * @param request: [HttpServletRequest] HttpServletRequest 객체 * @param response: [HttpServletResponse] HttpServletResponse 객체 */ public AccountDeleteProcess(HttpServletRequest request, HttpServletResponse response) { super(request, response); } /** * 연동 해제 응답 반환 메서드 * * @param accessCookie: [String] 접근 토큰 쿠키 * * @return [Response] 응답 객체 */ public Response deleteInfoResponse(String accessCookie) { Response response; ResponseBean<String> responseBean = new ResponseBean<>(); // 연동 해제 응답 생성 시도 try { Jws<Claims> jws = JwtModule.openJwt(accessCookie); String accessToken = jws.getBody().get("access", String.class); String platform = jws.getBody().get("platform", String.class); AuthModule authModule = getAuthModule(platform); // 연동 해제에 성공할 경우 if (authModule.deleteInfo(accessToken)) { response = new AccountPostProcess(request, this.response).postLogoutResponse(); } // 아닐 경우 else { throw new RequestAuthenticationException("revoke fail"); } } // 예외 catch (Exception e) { e.printStackTrace(); responseBean.setFlag(false); responseBean.setTitle(e.getClass().getSimpleName()); responseBean.setMessage(e.getMessage()); responseBean.setBody(null); response = Response.status(Response.Status.BAD_REQUEST).entity(responseBean).type(MediaType.APPLICATION_JSON).build(); } return response; } }
With this, the project's implementation is complete. Thanks to returning the different authentication modules — NaverAuthModule, GoogleAuthModule, and so on — through the shared parent object AuthModule, I was able to avoid complicated branching and duplicated code.
The moment a pipeline splits, the sub-pipelines connected to it also tend to be forced apart. Thanks to properly designing the lowest-level module, you can see that the upper pipelines built on top of it can be managed as a single unit.
The next chapter covers how to set up the controllers using Jersey.
