ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Spring Boot] Github OAuth 적용하기
    스터디 & 프로젝트/Init Cloud 팀 프로젝트 2023. 1. 31. 22:39

    Spring Boot를 활용해서 깃허브 앱을 만들 필요가 있었다.

    그런데 이 앱을 OAuth App으로 만들어야 하는지, Github App으로 만들어야 하는지 아니면 개인키로 인증 받아 권한만 받아와야하는지 조금 헷갈렸다.

    그래서 우선 Github OAuth를 적용하고 후에 인증 방식에 활용하기로 했다.

     


    앱, 클라이언트 ID 및 비밀 값 생성

    개인 또는 조직 설정 하단의 개발자 설정
    앱 생성

    우선 깃허브 개인 또는 조직 설정에서 개발자 설정을 찾는다. 개발자 설정에서 앱 유형을 선택하고 설정 페이지에서 앱을 생성하자.

    나는 각 앱 유형에 대한 이해가 아직 부족해서 우선 깃 허브 앱을 선택했다.

    URL 설정

    우선 URL 설정이 필요하다. 깃허브 앱이 동작할 URL과 OAuth 요청으로 리디렉션될 콜백 URL을 지정해야 한다.

    권한 설정

    OAuth를 통해 얻은 접근 권한으로 어디까지의 동작을 허용할 것인지 설정한다.

    클라이언트 ID, 클라이언트 시크릿

    앱이 생성되면 앱의 클라이언트 ID와 클라이언트 시크릿 값을 얻을 수 있다.

     


    OAuth 플로우

    왼쪽부터 클라이언트, (API) 서버, 인가 서버 및 자원 서버

    OAuth는 위 그림과 같은 플로우를 가진다.

    서버로 인가 요청을 보내면 서버는 가지고 있는 아래의 값과 함께 인가서버로 인가 요청을 보낸다.

    • 클라이언트 ID
    • Callback URI
    • 생성한 state

    해당 쿼리를 가진 상태로 클라이언트는 로그인을 위한 페이지로 리디렉션 된다.

    사용자가 로그인한다면 인가 서버는 인가 코드를 서버로 전달한다.

    이 코드 값은 일회성 값이며 최초로 인가서버로 보낸 Callback URI로 값이 전달 된다.

    인가 코드를 통해서 클라이언트가 서버로 인가 권한을 위임하게 되는 것이다.

    서버는 인가 코드 값과 함께 아래의 값들을 다시 인가 서버로 전달한다.

    • 클라이언트 ID
    • 클라이언트 비밀 값
    • Callback URI
    • state

    위 요청은 POST 요청으로 전달되며 인자 값은 URI 쿼리로 전달한다.

    인가 서버는 각 값을 판단하고 서버로 Access Token, Refresh Token과 함께 토큰 타입, 토큰 만료기한을 전달한다.

    이 Access Token을 통해 Github의 자원서버로 직접 요청을 보내 적절한 응답을 받을 수 있다.

    이 토큰을 서버에서 관리하게 되는 것이다.

     


    구현해보기

    @RestController
    @RequestMapping("/api/v1")
    @RequiredArgsConstructor
    public class GithubController {
    
        /* 사용할 서비스 객체 */
        private final GithubService githubService;
    
        /* 최초 요청 URI, 리디렉션 */
        @GetMapping("/auth")
        public void authorizeGithub(){
            githubService.requestGithubId();
        }
    
        /* 콜백 */
        @GetMapping("/callback")
        public ResponseEntity<String> callback(
                @RequestParam String code,
                @RequestParam String state){
    
                String response = githubService.requestAfterRedirect(code, state);
    
            return ResponseEntity.ok().body(response);
        }

     

     

    pubic class GithubService {
    
    	...
    
        @Override
        public void getAuthorize(HttpServletResponse response) {
            HttpRequest request = new HttpRequest();
            HttpParam.Query query = new HttpParam.Query();
    
            query.add("client_id", properties.getAppClientId());
            query.add("redirect_uri", properties.getCallback());
            query.add("state", Random.getUUID());
    
            String redirect = request.redirect(response, 
                                null, 
                                UrlEnv.URL_GITHUB_ID_GET.getUrl(), 
                                null, 
                                query.getQuery());
    
            try{
                response.sendRedirect(redirect);
            }catch (IOException e){
                throw new ApiException(ErrorCode.ERROR_5001);
            }
        }
    
        ...
    
        @Override
        public String callback(String code, String state){
            return (String)requestGithubAccessToken(code, state);
        }
    
        private Object requestGithubAccessToken(String code, String state) {
            try {
                HttpRequest request = new HttpRequest();
                HttpParam.Query query = new HttpParam.Query();
    
                query.add("client_id", properties.getAppClientId());
                query.add("client_secret", properties.getAppClientSecret());
                query.add("redirect_uri", properties.getCallback());
                query.add("code", code);
                query.add("state", state);
    
                return request.post(null, 
                                null, 
                                UrlEnv.URL_REDIRECT_POST.getUrl(), 
                                null, 
                                query.getQuery());
    
            } catch (Exception e) {
                throw new ApiException(ErrorCode.ERROR_5001);
            }
        }
    }

    이제 위와 같은 컨트롤러와 서비스 로직을 작성한다.

    코드 자체는 간단하다. 위 플로우를 그대로 코드로 옮긴 것 뿐이다.

    일부 코드가 생략되긴 했으나 유틸성 코드로 HTTP 요청이나 파라미터를 설정하는 함수 뿐이다.

    최종적으로 전달받는 Access Token은 Token 타입의 토큰이다.

     


    토큰을 통한 접근

    서버는 위 과정을 거쳐서 Access Token을 가지게 된다.

    이 토큰을 그대로 클라이언트로 전달해도 되지만 자체적인 Access Token을 생성하고 노출시키지 않은 상태로 관리할 수도 있다.

    어떤 경우라도 서버가 최초의 Access Token만을 가지고 있다면 자원서버로 적절한 요청을 보낼 수 있다.

     

    나의 경우 토큰을 통해서 처음 생성한 깃허브 앱이 설치된 조직, 레포지토리 등에 접근해야할 필요가 있다.

    따라서 이 토큰을 통해서 깃허브 API에 접근해야 한다.

     

    접근을 위해서는 HTTP Request Header에 다음과 같은 형태의 인자가 필요하다.

    Authorization : Token <Access_Token>

    Authorization : Bearer <Access_Token>

     

    전자의 경우 깃허브에서 제공하는 Access Token을 통한 접근 방식이다.

    이 방식의 경우 서버가 자원서버로 요청을 보내기 위해서 지정해야하는 토큰 타입과 크리덴셜이다.

     

    후자의 경우 JWT나 OAuth에 대한 토큰을 이용하는 방식으로 자체적으로 JWT 를 생성할 경우 이용할 것이다.

    클라이언트가 서버로 요청을 보내는 시점에서 필요하다,

     


    참고

    댓글

Designed by Tistory.