[OAuth2.0] Building an OAuth2.0 Authorization Server with ScribeJAVA - 10. Implementing the Controllers
[OAuth2.0] Building an OAuth2.0 Authorization Server with ScribeJAVA - 10. Implementing the Controllers
Let's set up the controllers that receive requests and return responses using the Jersey library.
If you want to learn more about Jersey, check out the previous post.
Before implementing the controllers, let's take care of a few configuration items.
- Specifying the URL for Jersey requests
- Configuring CORS
Those are the items.
This was also covered in a previous post. Just because Jersey is applied doesn't mean Jersey handles every request — you need to directly specify that Jersey should be delegated the requests.
Create a class in any package you like. The name doesn't matter. In this project, it was created as App.java in the main.java.global.module package.
JAVA
package global.module; import jakarta.ws.rs.ApplicationPath; import jakarta.ws.rs.core.Application; /** * 애플리케이션 클래스 * * @author RWB * @since 2021.09.29 Wed 22:40:20 */ @ApplicationPath("/api") public class App extends Application { // api 접두사 요청을 jersey가 담당 }
As shown above, extend the abstract class Application in the class and specify the desired URL prefix via @ApplicationPath.
Since we specified /api, every request starting with {BASE_URL}/api will be handled by Jersey. Every request that doesn't match this pattern will be handled normally by the Servlet.
Since the current demo project's API server is https://api.itcode.dev/oauth2, every request starting with https://api.itcode.dev/oauth2/api will be received by Jersey.
The URL configuration is done with just the one piece of code above.
The addresses for the demo project are as follows.
- Frontend - https://project.itcode.dev/oauth2
- Backend - https://api.itcode.dev/oauth2
As you can see, the domains for the requester and responder are different, so if you just send requests as-is, you'll almost certainly end up stuck in CORS hell.
To resolve this, we configure CORS on the server so requests can be sent to the desired domains.
Likewise, create a class in any package you like. The name doesn't matter. In this project, it was created as CorsFilter.java in the main.java.global.module package.
JAVA
package global.module; import jakarta.ws.rs.container.ContainerRequestContext; import jakarta.ws.rs.container.ContainerResponseContext; import jakarta.ws.rs.container.ContainerResponseFilter; import jakarta.ws.rs.ext.Provider; /** * CORS 필터 클래스 * * @author RWB * @since 2021.10.02 Sat 15:42:04 */ @Provider public class CorsFilter implements ContainerResponseFilter { /** * 필터 메서드 * * @param requestContext: [ContainerRequestContext] ContainerRequestContext 객체 * @param responseContext: [ContainerResponseContext] ContainerResponseContext 객체 */ @Override public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) { String origin = requestContext.getHeaderString("origin"); // origin이 유효하고, itcode.dev 계열의 URL일 경우 if (origin != null && origin.contains("itcode.dev")) { responseContext.getHeaders().add("Access-Control-Allow-Origin", origin); responseContext.getHeaders().add("Access-Control-Allow-Credentials", "true"); responseContext.getHeaders().add("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS"); responseContext.getHeaders().add("Access-Control-Allow-Headers", "Content-Type"); } } }
We implement the ContainerResponseFilter interface and override the filter method. This configuration applies globally to every operation Jersey performs.
It validates the Origin header, and if Origin has the itcode.dev domain, the response is allowed through the CORS configuration.
- Access-Control-Allow-Origin - the domain allowed to make the request
- Access-Control-Allow-Credentials - whether requests including credentials are allowed
- Access-Control-Allow-Methods - the HTTP methods allowed for the request
- Access-Control-Allow-Headers - the headers allowed for the request
Operations like login and logout include a Set-Cookie header that creates cookies, and when cookies need to be used across different domains like this, the server must set Access-Control-Allow-Credentials to true, and likewise the web client must specify credentials as true when making the request.
A request with credentials must specify an explicit domain!
The Access-Control-Allow-Origin header supports the wildcard *. If the header is set to *, the response is allowed regardless of domain. However, if Access-Control-Allow-Credentials is set to true, security policy requires that the domain be explicitly specified.
There are a total of 7 controllers to implement.
- LoginAPI (/api/login)
- Authorization URL API
- API to renew the consent-to-provide-information URL
- Login API
- Auto-login API
- LogoutAPI (/api/logout)
- Logout API
- RevokeAPI (/api/revoke)
- Unlink API
- UserInfoAPI (/api/userinfo)
- User info API
To manage common logic for the APIs as well, we implement an abstract class for every controller to extend.
Let's implement the abstract class API that every controller will extend.
JAVA
package global.module; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import jakarta.ws.rs.core.Context; import jakarta.ws.rs.core.UriInfo; /** * API 추상 클래스 * * @author RWB * @since 2021.09.29 Wed 22:34:27 */ abstract public class API { @Context protected HttpServletRequest request; @Context protected HttpServletResponse response; @Context protected UriInfo uriInfo; }
Given the nature of this project, there's no shared logic the API needs to use, so we declare the Servlet objects with the @Context annotation to access them efficiently.
Every controller that extends this will be able to freely access the Servlet objects.
The APIs LoginAPI is responsible for are as follows.
- LoginAPI (/api/login)
- Authorization URL API
- API to renew the consent-to-provide-information URL
- Login API
- Auto-login API
A total of 4 methods need to be declared.
JAVA
@Path("/login") public class LoginAPI extends API { // /api/login }
The controller object is implemented as above.
An API that returns the per-platform authorization URL to perform platform login.
Since the authentication object differs by platform, we need to distinguish the platform.
We distinguish the platform via @PathParam.
JAVA
@GET @Path("/{platform}") public Response authorizationUrlResponse(@PathParam("platform") String platform) { return new AccountGetProcess(request, response).getAuthorizationUrlResponse(platform); }
authorizationUrlResponse will handle GET /api/login/{platform} requests.
The @PathParam platform is assigned as the argument.
TXT
GET https://api.itcode.dev/oauth2/api/login/{platform}
| Field | Parameter Type | Data Type | Description |
|---|---|---|---|
| platform | Path | String | The platform name |
The platform name matches the platform's lowercase notation.
| Platform | Value | URL |
|---|---|---|
| NAVER | naver | GET /api/login/naver |
| GET /api/login/google | ||
| KAKAO | kakao | GET /api/login/kakao |
| GitHub | github | GET /api/login/github |
JSON
{ "flag": true, "title": "success", "message": "naver authrorization url response success", "body": "https://nid.naver.com/oauth2.0/authorize?response_type=code&client_id=czCaqAOB1aAjNRk6N_Oq&redirect_uri=https%3A%2F%2Fproject.itcode.dev%2Foauth2%2Fcallback%3Fplatform%3Dnaver&state=24ca41d9-f432-4e0d-9b48-e5fd4ba49e6e" }
| Parameter | Data Type | Description |
|---|---|---|
| flag | boolean | Whether the response is normal |
| title | String | The response title |
| message | String | The response message |
| body | String | The platform authorization URL |
The above request is an example of a response from https://api.itcode.dev/oauth2/api/login/naver.
An API that returns the URL for renewing consent to provide information.
JAVA
@PUT @Path("/put") public Response putAuthorizationUrlResponse(@CookieParam("access") String accessCookie) { return new AccountPutProcess(request, response).putUpdateAuthorizationUrl(accessCookie); }
putAuthorizationUrlResponse will handle PUT /api/login/{platform} requests.
The cookie named access is assigned as the argument.
TXT
PUT https://api.itcode.dev/oauth2/api/login/put Cookie: access={:access};
| parameter | type | data | required | description |
|---|---|---|---|---|
| {:access} | Cookie | String | Y | The authentication cookie |
JSON
{ "flag": true, "title": "success", "message": "naver reauthrorization url response success", "body": "https://nid.naver.com/oauth2.0/authorize?auth_type=reprompt&state=08199e0e-ef61-444a-8d4f-f3c048b771f0&response_type=code&client_id=czCaqAOB1aAjNRk6N_Oq&redirect_uri=https%3A%2F%2Fproject.itcode.dev%2Foauth2%2Fcallback%3Fplatform%3Dnaver" }
The above response is an example from https://api.itcode.dev/oauth2/api/login/put.
Since the platform is already included in the authentication info inside the access cookie, there's no need to distinguish the platform separately.
| parameter | data | description |
|---|---|---|
| flag | boolean | The operation result |
| title | String | The title |
| message | String | The content |
| body | String | The URL for renewing consent to provide information |
An API that performs login.
It distinguishes the platform.
JAVA
@POST @Path("/{platform}") public Response loginResponse(@PathParam("platform") String platform, LoginResponseBean loginResponseBean) { return new AccountPostProcess(request, response).postLoginResponse(platform, loginResponseBean.getCode(), loginResponseBean.getState()); }
loginResponse will handle POST /api/login/{platform} requests.
TXT
POST https://api.itcode.dev/oauth2/api/login/{:platform} { "code": {:code}, "state": {:state} }
| parameter | type | data | required | description |
|---|---|---|---|---|
| {:platform} | path | String | Y | Platform (lowercase notation) |
| {:code} | body | String | Y | The access code |
| {:state} | body | String | Y | A unique state value |
The platform name matches the platform's lowercase notation.
| Platform | Value | URL |
|---|---|---|
| NAVER | naver | POST /api/login/naver |
| POST /api/login/google | ||
| KAKAO | kakao | POST /api/login/kakao |
| GitHub | github | POST /api/login/github |
TXT
Set-Cookie: access={access} Set-Cookie: refresh={refresh} { "flag": true, "title": "success", "message": "authorized success", "body": null }
| Parameter | Data Type | Description |
|---|---|---|
| flag | boolean | Whether the response is normal |
| title | String | The response title |
| message | String | The response message |
| body | null | null |
The Set-Cookie header automatically adds the token carrying the authentication info.
An API that automatically performs login without any interaction, using existing authentication info still around.
JAVA
@POST @Path("/auto") public Response autoLoginResponse(@CookieParam("access") String accessCookie, @CookieParam("refresh") String refreshCookie) { return new AccountPostProcess(request, response).postAutoLoginResponse(accessCookie, refreshCookie); }
It validates the access cookie and refresh cookie, and if there's no issue, it automatically performs login either by checking the cookie info or by reissuing the Access Token.
Since the platform info is already included inside the cookie, there's no need to distinguish the platform.
autoLoginResponse will handle POST /api/login/auto requests.
TXT
POST https://api.itcode.dev/oauth2/api/login/auto Cookie: access={:access}; refresh={:refresh};
| parameter | type | data | required | description |
|---|---|---|---|---|
| {:access} | Cookie | String | N | The authentication cookie |
| {:refresh} | Cookie | String | Y | The refresh cookie |
JSON
{ "flag": true, "title": "success", "message": "auto authorized success", "body": null }
TXT
# Header Set-Cookie: access={access JWT} Set-Cookie: refresh={refresh JWT}
Set-Cookie is only included when only the refresh cookie was present and the Access Token was refreshed.
| parameter | data | description |
|---|---|---|
| flag | boolean | The operation result |
| title | String | The title |
| message | String | The content |
| body | null | null |
The Set-Cookie header automatically adds the token carrying the authentication info.
If the access cookie is still alive, there's no need to create a separate cookie, so the Set-Cookie header isn't sent.
JAVA
package oauth.account.controller; import global.module.API; import jakarta.ws.rs.CookieParam; import jakarta.ws.rs.GET; import jakarta.ws.rs.POST; import jakarta.ws.rs.PUT; import jakarta.ws.rs.Path; import jakarta.ws.rs.PathParam; import jakarta.ws.rs.core.Response; import oauth.account.bean.LoginResponseBean; import oauth.account.process.AccountGetProcess; import oauth.account.process.AccountPostProcess; import oauth.account.process.AccountPutProcess; /** * 로그인 API 클래스 * * @author RWB * @since 2021.09.30 Thu 20:44:43 */ @Path("/login") public class LoginAPI extends API { /** * 인증 URL 응답 메서드 * * @param platform: [String] 플랫폼 * * @return [Response] 응답 객체 */ @GET @Path("/{platform}") public Response authorizationUrlResponse(@PathParam("platform") String platform) { return new AccountGetProcess(request, response).getAuthorizationUrlResponse(platform); } /** * 정보 제공 동의 갱신 URL 응답 메서드 * * @param accessCookie: [String] 접근 토큰 쿠키 * * @return [Response] 응답 객체 */ @PUT @Path("/put") public Response putAuthorizationUrlResponse(@CookieParam("access") String accessCookie) { return new AccountPutProcess(request, response).putUpdateAuthorizationUrl(accessCookie); } /** * 로그인 응답 메서드 * * @param platform: [String] 플랫폼 * @param loginResponseBean: [LoginResponseBean] LoginResponseBean 객체 * * @return [Response] 응답 객체 */ @POST @Path("/{platform}") public Response loginResponse(@PathParam("platform") String platform, LoginResponseBean loginResponseBean) { return new AccountPostProcess(request, response).postLoginResponse(platform, loginResponseBean.getCode(), loginResponseBean.getState()); } /** * 자동 로그인 응답 메서드 * * @param accessCookie: [String] 접근 토큰 쿠키 * @param refreshCookie: [String] 리프레쉬 토큰 쿠키 * * @return [Response] 응답 객체 */ @POST @Path("/auto") public Response autoLoginResponse(@CookieParam("access") String accessCookie, @CookieParam("refresh") String refreshCookie) { return new AccountPostProcess(request, response).postAutoLoginResponse(accessCookie, refreshCookie); } }
The APIs LogoutAPI is responsible for are as follows.
- LogoutAPI (/api/logout)
- Logout API
A total of one method needs to be declared.
JAVA
@Path("/logout") public class LogoutAPI extends API { // /api/logout }
The controller object is implemented as above.
An API that performs logout.
It deletes the authentication info stored in cookies.
JAVA
@POST @Path("") public Response logoutResponse() { return new AccountPostProcess(request, response).postLogoutResponse(); }
autoLoginResponse will handle POST /api/login/auto requests.
There are no separate arguments, since both the access and refresh cookies will be deleted regardless of whether they exist.
TXT
POST https://api.itcode.dev/oauth2/api/logout
JSON
{ "flag": true, "title": "success", "message": "logout success", "body": null }
TXT
# Header Set-Cookie: access={access JWT} Set-Cookie: refresh={refresh JWT}
| parameter | data | description |
|---|---|---|
| flag | boolean | The operation result |
| title | String | The title |
| message | String | The content |
| body | null | null |
The cookies are deleted by overwriting them with Set-Cookie cookies with Max-Age 0.
JAVA
package oauth.account.controller; import global.module.API; import jakarta.ws.rs.POST; import jakarta.ws.rs.Path; import jakarta.ws.rs.core.Response; import oauth.account.process.AccountPostProcess; /** * 로그아웃 API 클래스 * * @author RWB * @since 2021.10.04 Mon 21:19:00 */ @Path("/logout") public class LogoutAPI extends API { /** * 로그아웃 응답 메서드 * * @return [Response] 응답 객체 */ @POST @Path("") public Response logoutResponse() { return new AccountPostProcess(request, response).postLogoutResponse(); } }
The APIs RevokeAPI is responsible for are as follows.
- RevokeAPI (/api/revoke)
- Unlink API
A total of one method needs to be declared.
JAVA
@Path("/revoke") public class RevokeAPI extends API { // /api/revoke }
The controller object is organized as above.
An API that completely unlinks from the platform.
JAVA
@DELETE @Path("") public Response deleteInfoResponse(@CookieParam("access") String accessCookie) { return new AccountDeleteProcess(request, response).deleteInfoResponse(accessCookie); }
deleteInfoResponse will handle DELETE /api/revoke requests.
TXT
DELETE https://api.itcode.dev/oauth2/api/revoke Cookie: access={:access};
| parameter | type | data | required | description |
|---|---|---|---|---|
| {:access} | Cookie | String | Y | The authentication cookie |
JSON
{ "flag": true, "title": "success", "message": "logout success", "body": null }
TXT
# Header Set-Cookie: access={access JWT} Set-Cookie: refresh={refresh JWT}
| parameter | data | description |
|---|---|---|
| flag | boolean | The operation result |
| title | String | The title |
| message | String | The content |
| body | null | null |
JAVA
package oauth.account.controller; import global.module.API; import jakarta.ws.rs.CookieParam; import jakarta.ws.rs.DELETE; import jakarta.ws.rs.Path; import jakarta.ws.rs.core.Response; import oauth.account.process.AccountDeleteProcess; /** * 연동 해제 API 클래스 * * @author RWB * @since 2021.10.18 Mon 01:19:30 */ @Path("/revoke") public class RevokeAPI extends API { /** * 연동 해제 URL 응답 메서드 * * @param accessCookie: [String] 접근 토큰 쿠키 * * @return [Response] 응답 객체 */ @DELETE @Path("") public Response deleteInfoResponse(@CookieParam("access") String accessCookie) { return new AccountDeleteProcess(request, response).deleteInfoResponse(accessCookie); } }
The APIs UserInfoAPI is responsible for are as follows.
- UserInfoAPI (/api/userinfo)
- User info API
A total of one method needs to be declared.
JAVA
@Path("/userinfo") public class UserInfoAPI extends API { // /api/userinfo }
The controller object is implemented as above.
An API that returns user info based on the Access Token.
JAVA
@GET @Path("") public Response userInfoResponse(@CookieParam("access") String accessCookie) { return new AccountGetProcess(request, response).getUserInfoResponse(accessCookie); }
userInfoResponse will handle GET /api/userinfo requests.
Rather than returning the platform's response as-is, it processes it appropriately according to each platform's response schema and provides a standardized response.
TXT
GET https://api.itcode.dev/oauth2/api/userinfo Cookie: access={:access};
| parameter | type | data | required | description |
|---|---|---|---|---|
| {:access} | Cookie | String | Y | The authentication cookie |
JSON
{ "flag": true, "title": "success", "message": "user info response success", "body": { "email": "example@gmail.com", "name": "name", "profile": "https://phinf.pstatic.net/contact/PROFILE.png", "platform": "naver" } }
The above response is an example of a Naver user info response.
| parameter | data | description |
|---|---|---|
| flag | boolean | The operation result |
| title | String | The title |
| message | String | The content |
| body | Object | The user info JSON |
| String | The email | |
| name | String | The name |
| profile | String | The profile picture URL |
| platform | String | The platform |
JAVA
package oauth.account.controller; import global.module.API; import jakarta.ws.rs.CookieParam; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; import jakarta.ws.rs.core.Response; import oauth.account.process.AccountGetProcess; /** * 사용자 정보 API 클래스 * * @author RWB * @since 2021.10.02 Sat 00:29:46 */ @Path("/userinfo") public class UserInfoAPI extends API { /** * 사용자 정보 응답 메서드 * * @param accessCookie: [String] 접근 토큰 쿠키 * * @return [Response] 응답 객체 */ @GET @Path("") public Response userInfoResponse(@CookieParam("access") String accessCookie) { return new AccountGetProcess(request, response).getUserInfoResponse(accessCookie); } }
With this chapter, the implementation of the authorization server's major components — controller, process, and module — is complete.
Since the implementation of the authorization server is finished, the next chapter covers the review process for wrapping up the project.
