본문으로 바로가기

Authorization Code Grant 는 OAuth 2.0 인증 유형 중 하나로,

사용자가 권한 제공 기관에 연결하여 인증 받고 권한 부여에 동의하면 승인 코드를 발급해주는 유형이다.

 

해당 코드(Authorization code)를 통해 Access Token 을 받을 수 있으며

해당 Access Token 을 가지고 여러 서비스(구글 계정 정보 등)을 얻을 수 있다.

 

Authorization code 를 받는 이유? 

웹 브라우저 <-> Provider ( Authorization Server ) 로 Access Token 을 받는 것보다

Client(Application) <-> Provider 로 Token 을 주는게 보안 상 더 좋다고 한다.

 

 

해당 포스트에서는 Google API Console + SpringBoot를 활용한 Authorization Code 방식 구현 코드를 작성했다.

키워드 : Intellij + SpringBoot + Maven + Java8 + Oauth 2.0 + Google Api Console

목적 : 코드 구현을 통한 Authorization Code Grant 흐름 파악 ( 누가 어떤 정보를 요청하고 누가 어떤 정보를 주고 받는 지 등 )

* Spring Security + Oauth2.0 구현이 아니다.  

 

먼저, 사용할 Google API Console 등록 과정 및 용어 설명을 진행한다.

[1. Google Api Console 등록 과정]

(등록) 클라이언트가 Resource Server 를 이용하기 위해서 승인을 사전에 받는 과정이 필요하다.

서비스에 따라서 등록 방법은 다르지만 공통적으로 

- Client ID

- Client Secret

- Authorized redirect URL

등은 사용되는 것 같다.

 

 

등록 과정은 다음과 같다.

- 프로젝트 생성

- 사용자 인증 정보 생성

 

-- 1.1 프로젝트 생성

Google API Console 접속 및 프로젝트 생성

console.developers.google.com/

 

Google Cloud Platform

하나의 계정으로 모든 Google 서비스를 Google Cloud Platform을 사용하려면 로그인하세요.

accounts.google.com

 

-- 1.2 사용자 인증 정보 생성 (= OAuth Client ID 생성)

사용자 인증 정보 생성 > OAuth Client ID 생성 클릭 및 각 종 사항 기입

 

> OAuth 동의 화면 작성

- User Type : 외부

- 이름과 이메일 작성 후 Next & Finish

 

> 완료 후 사용자 인증정보 >  OAuth Client ID 생성 다시 클릭

- 애플리케이션 유형 : 웹 프로젝트

- 승인된 리다이렉션 URI : Callback 에 사용할 URL 작성

     (ex. http://localhost:8080/callback

         * 만약 Postman 으로 테스트 할 경우, http://oauth.pstmn.io/v1/callback )

 

> Client ID & Client Secret 발급

 

 

-- 용어 정리

redirect uri : 서비스 제공자가 인가 요청에 대한 응답을 전달하는 URL (= Authorization code 를 돌려 받을 URL)

client id : 어떤 Client 가 OAuth 인증을 요청하는 지 Google 서버에서도 알아야 하기 때문에 필요

client secret : 비밀키, 해당 키를 통해 토큰을 생성

 

response_type : 어떤 OAuth 인증 방식을 사용할 것인가

                  - token : Implicit Grant 사용 시 사용

                  - code : Authorization Code Grant 사용 시 사용

 

state : 사이트 간 CSRF 공격을 막기 위해 애플리케이션에서 사용되는 예측 불가한 값으로 사용되는 것(애매)

 

access_type : 사용자가 브라우저를 나가도 Refresh Token을 받아서 쓸 수 있는 지 여부

                      - offline : Access Token 을 받을 때 Refresh Token 을 같이 받는다. (= 만료 시 갱신 필요한 경우 사용)

                      - online : 

 

scope : 사용하고자 하는 API 서비스, 사용자 로그인 시 명시적으로 동의를 받는다.

             즉, 사용자의 어떤 데이터와 어떤 작업에 대한 권한을 요청하는 지를 나타낸다.

             if ) Access Token 이 반환되는 경우, Access Token의 유효 범위를 나타낸다.

 

token_type : 발급된 Access Token 의 타입 (보통 Bearer)

expires_in : Access Token 이 만료되기 까지 남은 시간 (초 단위)

                   해당 유효 기간에만 API 호출이 가능하다.

 

id_token : JWT(Json Web Token) 토큰 값으로 토큰 인증에 사용된다.

              * Client 에서 서비스 제공 요청 시 -> 서버는 토큰 무결성 체크 & payload 정보를 사용해 데이터를 넘겨준다.

               

 

[2. 동작 과정 (=시나리오)]

* Resource Server / Authorization Server 역할은 Google 에서 수행한다.

1. 사용자(Resource Owner) 구글 로그인 수행(= 사용자의 승인)

2. 로그인 시, 구글(Authorization Server)에서 code 라는 파라미터를 붙여서 redirection_url 로 callback 수행

3. code + grant_type + redirection_url  + client_id + client_secret 등을 담아서 구글에게 재 요청 (= Access Token 달라고 요청)

4. 구글(Authorization Server)은 해당 Access Token 을 비교

                                 ->  access_token + expires_in + scope + token_type + id_token 반환 

5. 해당 access_token 을 가지고 "Google 계정 정보" 등 서비스를 요청

6. 구글(Resource Server)에서 해당 서비스 제공

 

 

[3. 구현]

-- 로그인 창

웹 브라우저 로그인 창을 만들기 위해 view로는 Thymeleaf templates 을 사용한다.

<!DOCTYPE html>
<html>
<head>
    <title>Login</title>
</head>
<body>
<a href="
https://accounts.google.com/o/oauth2/v2/auth?
scope=https://www.googleapis.com/auth/cloud-identity.devices.lookup&
access_type=offline&
include_granted_scopes=true&
state=state_parameter_passthrough_value&
redirect_uri=<Redirect URL 기입>&
response_type=code&
client_id=<클라이언트 아이디 기입>
">Login with Google</a>
</body>
</html>

 

-- 구현 코드

* 테스트 용이므로, 하나의 클래스 안에 모두 작성했다.

 

설정 정보는 가져오기 나름이다. 

    // 
    private static final String CLIENT_ID = "";
    private static final String CLIENT_SECRET = "";
    private static final String REDIRECT_URI = "";
    private static final String GRANT_TYPE = "";
    private static final String ACCESS_TOKEN_URL = "";
   

 

첫 시작으로, localhost:port/로 접속 시 로그인 창이 뜨도록 설정한다.

    /**
     * Login with Google 1 : Authorization Code Grant
     */
    @GetMapping("/")
    public String login() {
        return "login";
    }

 

http://localhost:port/callback 이 Redirect URL 로 등록되어 있기 때문에 /callback 으로 code 값을 받는다.

GoogleSignCallback() 에서는 Access Token 을 얻기 / 사용자 정보 얻기 를 수행하도록 구현했다.

    /**
     * Callback 1 : Authorization Code Grant
     */
    @RequestMapping(value = "/callback", method = RequestMethod.GET)
    public String GoogleSignCallback(HttpServletRequest request, HttpServletResponse response) throws Exception {

        log.info("## [REQUEST] code ={}", request.getParameter("code"));
        try {
            /* Access Token 얻기 */
            String accessToken = getGoogleAccessToken(request, response);
            /* 사용자 정보 얻기 */
            getGoogleUserInfo(accessToken);
        } catch (Exception e) {
            log.error(e);
            throw e;
        }
        /* 임시 - 로그인 성공 창 */
        return "home";
    }

code 값을 담아서 Access Token 을 달라고 요청한다.

    /**
     * Callback 1 : Authorization Code Grant
     * Access Token 처리 (+획득)
     *
     * @param request
     * @Param response
     * @throws Exception
     * @return Access Token 값 
     */
    public String getGoogleAccessToken(HttpServletRequest request, HttpServletResponse response) throws Exception {

        String code = request.getParameter("code");

        HttpHeaders headers = new HttpHeaders();
        RestTemplate restTemplate = new RestTemplate();
        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);

        MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
        parameters.add("code", code);
        parameters.add("client_id", CLIENT_ID);
        parameters.add("client_secret", CLIENT_SECRET);
        parameters.add("redirect_uri", REDIRECT_URI);
        parameters.add("grant_type", GRANT_TYPE);

        HttpEntity<MultiValueMap<String, String>> rest_request = new HttpEntity<>(parameters, headers);

        URI uri = URI.create(ACCESS_TOKEN_URL);

        ResponseEntity<String> responseEntity;
        responseEntity = restTemplate.postForEntity(uri, rest_request, String.class);
        String bodys = responseEntity.getBody();
        log.info("## BODYS = {}", bodys);

        JsonParser jsonParser = new JsonParser();
        JsonElement jsonElement = jsonParser.parse(bodys);

        return jsonElement.getAsJsonObject().get("access_token").getAsString();
    }

Access Token 을 가지고 사용하고자 하는 서비스를 제공해 달라고 요청한다.

* 이 코드에서는 사용자 계정 정보를 제공해 달라고 요청한다.

    /**
     * Callback 1 : Authorization Code Grant
     * Google 사용자 계정 정보 가져오기
     *
     * @param accessToken 획득한 ACCESS TOKEN 값
     * @throws
     */
    public void getGoogleUserInfo(String accessToken) throws Exception {

        //요청하는 클라이언트마다 가진 정보가 다를 수 있기에 HashMap 선언
        HashMap<String, Object> googleUserInfo = new HashMap<>();

        String reqURL = "https://www.googleapis.com/oauth2/v1/userinfo?alt=json&access_token=" + accessToken;
        try {
            URL url = new URL(reqURL);
            HttpURLConnection conn = (HttpURLConnection) url.openConnection();

            //요청에 필요한 Header에 포함될 내용
            conn.setRequestProperty("Authorization", "Bearer " + accessToken);

            int responseCode = conn.getResponseCode();
            log.info("## ResponseCode : {}", responseCode);

            if (responseCode == 200) {
                BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream()));

                String line = "";
                String result = "";
                while ((line = br.readLine()) != null) {
                    result += line;
                }

                JsonParser parser = new JsonParser();
                log.info("## Result = {}", result);

                JsonElement element = parser.parse(result);
                String name = element.getAsJsonObject().get("name").getAsString();
                String email = element.getAsJsonObject().get("email").getAsString();
                String id = "GOOGLE_" + element.getAsJsonObject().get("id").getAsString();

                googleUserInfo.put("name", name);
                googleUserInfo.put("email", email);
                googleUserInfo.put("id", id);

                log.info("## Login Controller : {}", googleUserInfo);
            }
        } catch (Exception e) {
            log.error(e);
            throw e;
        }

    }

 

[추가 정보]

1. Access Token 만료 기간이 지나면 Refresh Token 값을 통해 갱신을 할 수 있다고 한다.

* Refresh Token 갱신에 대해서는 아직 해보지 않았다.

* Refresh Token 값은 Access Token 값 수신 시 같이 받는다.

 

2. Access Token 값을 얻기 위해 code 값을 전송해주면 provider 는 Access Token 을 전송해주고 해당 code 값을 폐기한다.

* 일회용 code

 

 

 

 

 

# 각 종 블로그 내용과 공홈 내용을 참고로 작성 & 정리 및 실습했습니다.