OAuth2란?
'내가 서비스 중인 어플리케이션에 다른 서비스를 운영하는 사람이 접근하려면 어떻게 해야할까?'
예를 들어, 파일을 인쇄해 주는 서비스를 하고 있는데 그 사진을 관리하는 서비스에서 사진들을 가져와야 한다면 서로 어떻게 사용자를 인식하고 정상적으로 운영할 수 있을까요?
이러한 상황에서 사용하는 프로토콜이 OAuth 프로토콜입니다.
OAuth2가 어떤 프로토콜이길래 가능한 거지?
OAuth는 내 서비스를 사용하고자 하는 서비스에게 접근 권한을 제공하여 이 접근 권한만 있으면 언제든 접근할 수 있도록 사용자와 내 서비스 간 인증 및 인가 프로토콜이라 생각할 수 있습니다.
OAuth2의 구성
앞으로 설명할 내용에서 꼭 알고 가야 할 역할
이라는 개념이 있습니다. OAuth를 구성한 각각의 서버나 클라이언트에 대한 것이고, Resource Owner
, OAuth Client
, Authorization Server
, Resource Server
가 있습니다.

Resource Owner ( 사용자 )
: 우리가 흔히 생각하는고객
입니다.OAuth Client
: 개발하는 사이트 or 예제에서의 파일 인쇄 서비스Authorization Server
: 사용자 인증 및 토큰을 발급해 주는 서버Resource Server
: 사용하고자 하는 서비스를 제공해 주는 서버
그런데, 위 그림에서는 OAuth Server
로 뭉쳐서 그렸는데 사실 OAuth는 OAuth1.0에서 OAuth2.0으로 버전 업을 했습니다. OAuth1.0에서는 OAuth Server가 Resource Owner에 대한 인증, 인가 토큰 발급, 리소스 관리까지 모든 책임을 맡았지만, 역할이 분리되어있지 않았고 이러한 문제를 해결하기 위해 OAuth2.0에서는 역할을 분리하기 위해 서버를 분리한 것입니다.
Client 등록
구글 OAuth 서비스를 등록하기 위한 절차입니다. Google Cloud consol에서 시작할 수 있습니다.
등록 과정
먼저 대시보드에서 프로젝트를 생성할 수 있습니다.

이후 OAuth동의화면에서 배포하여 모든 사용자가 사용할 수 있도록 외부를 선택합니다.

이후 필수 입력 정보들을 넣으면 됩니다.

도메인과 사용자 약관등을 제공해야 하는데, 현재는 테스트를 위해서 작성하지 않겠습니다.

승인된 도메인도 테스트에서는 필요하지 않으므로 넘기겠습니다.

제공해 줄 데이터들을 추가해 줍니다.

이후 사용자 인증 정보
로 가면 됩니다.

이제 Redirection URI를 설정하면 됩니다. redirection uri는 아래에서 추가 설명이 있습니다.

이후 클라이언트 등록이 완료되었습니다.

여기서 client_id
와 client_secret
의 경우 OAuth 설정할 때 사용하기 때문에 잘 복사해야 합니다.
Scope
OAuth2를 적용하기 위해 클라이언트를 등록하는 과정에서 범위 (Scope)라는 개념이 나옵니다.
Scope는 기존 OAuth1.0에서는 토큰이 발급되면 모든 값에 접근이 가능했는데 여기에 문제가 있습니다.

저는 B에서 제공해 주는 특정 서비스를 이용하기 위해서 OAuth를 사용한 것이고, B서비스에서도 특정 서비스만을 제공하면 되는데 그 외의 것도 제공해 준다는 문제가 발생했습니다.

이렇게 모든 데이터를 접근을 할 수 있는 것입니다.
그렇기에 OAuth2.0으로 넘어오면서 우리가 등록을 할때 사용하고자 하는 범위를 정해놓고 그 범위를 초과하면 사용하지 못하도록 막아놓고 있습니다.
Google Scope - Google OAuth 정책
Redirect URI
OAuth2.0은 인증이 성공한 사용자를 사전에 등록된 Redirect URI로만 redirect시키며, TLS가 적용되어 있어야한다.
인증서버는 TLS 사용을 의무화하지는 않지만, TLS를 사용하지 못한다면 해당 엔트포인에 경고를 해야합니다.
관련 링크 - Endpoint Request Confidentialiy
하지만 Authorization server 즉 토큰을 발행하는 서버의 경우 반드시 TLS를 사용해야 합니다.

관련 링크 - Client Password
Client ID & Client Sercet
Client id
는 client를 등록할 때 받는 Unique한 문자열이다. 비문이 아니고 사용자에게 노출이 된는 값입니다. 따라서 홀로 인증에 사용되면 안되는 값이라 볼 수 있습니다.
Client Secret
은 password로써 HTTP Basic 인증 스키마를 활용한 인증 서버와 인증을 수행할 수 있다 나와있습니다. 결국 Token방식을 통해 동작하게 되는 것인데 token의 문제점 중 하나인 소유만으로 인증 인가가 완료된다는 문제가 남아있습니다. 따라서 이러한 문제를 해소하기 위해 TLS를 사용하여 문제를 해결하였습니다.
관련 링크 - Client Password
OAuth2의 동작
OAuth2의 동작에 대해 알아보겠습니다.

먼저 동작 flow입니다
1. 사용자의 로그인
사용자가 B라는 사이트에 접근 권한을 가져가기 위해서는 로그인을 해야합니다. 그렇다면 B라는 사이트에 접근하여 로그인을 해야할까요?
이렇게 되면 B사이트에 접근이 가능하더라도 우리 사이트(A)에는 접근을 못합니다. 따라서 로직의 흐름 자체를 A가 가져가야합니다.
따라서 사용자는 우리 사이트에서 OAuth2 프로토콜을 통해 인가를 할 수 있는 로그인이 존재해야합니다.

이렇게 로그인 버튼으로 서버에 사용자 요청을 보내게 됩니다.
GET /login/google
그러면 서버에서는 B 서비스에 접근하기 위한 접근 권한이 있어야 합니다. 따라서 인증 서버에 redirect해줘야 합니다.
인증 요청
클라이언트는 Authorization Server에 인증 요청을 보내기 위해 요청 URI를 구성해야 합니다.
Authroization Request
필수
- response_type: confidential client이기 때문에
code
값이 들어가야 합니다. - client_id: 클라이언트 등록시 받은 ID
옵션
- redirect_uri: redirect할 URI
- scope: 접근 허용 범위
추가로 state
는 필수는 아니지만 추천한 파라미터입니다. csrf요청 위조를 방지하기 위해 사용합니다.
또한 application/x-www-form-urlencoded
포멧을 사용합니다.
어떻게 인증 요청을 구성해야할까?
OAuth의 규격을 정의한 rfc6749에서는

요청 URI구성와 위와 같은 예시가 있습니다. 이를 기반으로
Google의 OAuth Redirection 부분을 보면

엔드 포인트와 매개변수를 확인할 수 있고,
private static final String GOOGLE_URL = "https://accounts.google.com/o/oauth2/v2/auth?";
private static final String CLIENT_ID = "클라이언트 등록 ID";
private static final String REDIRECT_URI = "http://localhost:8080/oauth/google";
private static final String RESPONSE_TYPE = "code";
private static final String SCOPE = "https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile";
private static final String ACCESS_TYPE = "offline"; // online or offline
@GetMapping("/login/google")
@ResponseStatus(HttpStatus.FOUND)
public void redirectGoogleLogin(HttpServletResponse response) throws IOException {
StringBuilder authenticationUrl = new StringBuilder(GOOGLE_URL);
authenticationUrl.append("client_id=").append(CLIENT_ID)
.append("&redirect_uri=").append(REDIRECT_URI)
.append("&response_type=").append(RESPONSE_TYPE)
.append("&scope=").append(SCOPE)
.append("&access_type=").append(ACCESS_TYPE);
response.setContentType("application/x-www-form-urlencoded");
response.sendRedirect(authenticationUrl.toString());
}
와 같이 URI를 만들고 Content-type은 application/x-www-form-urlencoded
로 설정하여 보낼 수 있습니다.
scope의 구분
scope를 매개변수로 제공할때 값은 공백으로 구분하라고 명시되어 있으며, Google scope에서 추가적으로 필요한 값을 넣으면 됩니다.

Redirect URI 결과

해당 페이지로 redirect한 것을 볼 수 있고

로그인 화면이 정상동작하는 것을 볼 수 있습니다.
만약, 다른 페이지가 뜬다면 잘못된 요청을 보낸 것이기 때문에 다시 한번 확인해 보시기 바랍니다.
이후, 로그인을 하게 되면

code
, scope
, authuser
, prompt
를 반환받게 된다.
Authorization Code
인증 코드를 사용자에게 제공하고, 사용자는 다시 클라이언트에게 제공하여 토큰을 발급받을 수 있도록 하기위 한 값입니다. 따라서 Code값은 최소 시간을 가지고 있어야 합니다.
왜 Access Token이 아닌 Authorization Code를?
모든 페이지가 TLS가 적용되어있지 않는데, URL 매개 변수에 엑세스 토큰을 전달한다는 것은 인증 및 인가 역할만 하고 아무런 보호 조치가 없는 Token이 탈취되었을 때의 리스크가 너무 큽니다. 따라서 암호화되지 않는 트랜잭션을 통해 요청을 가로채는 악의적인 사용자가 있을 수 있으므로 인증 코드를 통해 전달하게 됩니다.
여기까지하면 사용자가 로그인 요청을하고 로그인을 마치면서 Authorization Code
를 발급받는 것 까지 했습니다.
다음으로 넘어가기 전에 한 가지 의문이 드실 수 있을 거 같은데 '왜 Client
가 Authorization Server
에 요청을 보낼때 GET이 아닌 Redirect로 보낸것인가?' 일거 같습니다. 왜냐하면 위에서도 요청URI가 GET으로 되어있기 때문입니다.
그 이유는 사용자가 직접 로그인을 하고 그 로그인이 완료되면 그에 따른 Authroization Code를 반환해야 하는데, 바로 Get으로 새로운 요청을 보내게 되면, 먼저 Response를 받고 사용자가 로그인을 하게 되므로 순서에 맞지 않아서 입니다.
2. Redirect URI with Authorization code & Get Access Token
1번에서는 사용자가 Authorization Code를 다시 받는 거 까지 봤습니다. 이제 이 code값을 통해 access token을 발급받으면 됩니다.
redirect를 하게 된다면 해당 url이 필요합니다. 따라서
GET /oauth/code/google
를 만들어 줘야합니다. 그 후에는 여러 파라미터로 받은 값을 매핑해줘야 합니다.
@GetMapping("/oauth/code/google")
public String authorizationCode(@RequestParam("code") String code,
@RequestParam("scope") String scope,
@RequestParam("authuser") String user,
@RequestParam("prompt") String prompt) {
}
Token 요청
필수
- grant_type:
authorication_code
를 필수적으로 입력해야 한다. - code: 반환받은 code값을 제공
- redirect_uri: redirect할 uri제공
- client_id: 클라이언트 생성시 받은 ID
해당 필수 내용은 Access Token Request에서 확인이 가능하며, Google의 경우 추가적인 값이 필요합니다.
바로 client_secret
이 필요합니다.
관련 정보 - Google OAuth Token Request
위 정보들을 토대로

이렇게 보내면 된다. 따라서
@GetMapping("/oauth/google")
public String authorizationCode(@RequestParam("code") String code,
@RequestParam("scope") String scope,
@RequestParam("authuser") String user,
@RequestParam("prompt") String prompt) throws IOException, URISyntaxException {
MultiValueMap<String, String> oauthParam = new LinkedMultiValueMap<>();
oauthParam.add("code", code);
oauthParam.add("client_id", CLIENT_ID);
oauthParam.add("client_secret", CLIENT_SECRET);
oauthParam.add("redirect_uri", REDIRECT_URI);
oauthParam.add("grant_type", AUTHORIZATION_CODE);
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8");
HttpEntity httpEntity = new HttpEntity(oauthParam, httpHeaders);
return new RestTemplate().exchange(
AUTHORIZATION_SERVER_URL,
HttpMethod.POST,
httpEntity,
String.class
);
}
와 같이 파라미터를 넣어주고 Content-type
을 application/x-www-form-urlencoded
로 설정하여 body의 파라미터가 json이 아닌 &
로 분리되며 String으로 전달가능하게 하면 됩니다.
이렇게 되면 Authorization Server
에서 전달 받은 Response를 받아야하는데
필수적으로 들어가야 하는 Response는 access_token
, token_type
, expires_in
, refresh_token
이 있습니다. 또한 Google에서는 scope를 함께 받기 때문에

위와 같이 받을 수 있습니다.
3. Resource 받기
원래는 DB에 저장하던지 아님 새로운 토큰 발급하던지 해야하지만 이후 이야기하고 싶은 부분이어서 빼고 간략하게만 했습니다.
2번까지 완료했다면 Authorization Server에서 response를 받았으므로 이를 parsing한 후 쿠키에 담아야합니다 그래야 다른 요청에 대해서도 쿠키에서 token만 가져와서 진행할 수 있으니까요
ResponseEntity responseToken = new RestTemplate().exchange(
AUTHORIZATION_SERVER_URL,
HttpMethod.POST,
httpEntity,
String.class
);
ObjectMapper objectMapper = new ObjectMapper();
oAuthTokenResponse = objectMapper.readValue(responseToken.getBody().toString(), OAuthTokenResponse.class);
Cookie cookie = new Cookie("Authorization", oAuthTokenResponse.accessToken());
cookie.setPath("/");
response.addCookie(cookie);
위와 같이 Authorization Server에서 받은 응답값 responseToken에서 body를 가져와 access token만 쿠키에 넣는 코드입니다. 이렇게 한다면

쿠키에 들어가는 것을 볼 수 있는데, 여기서 주의해야할 점은 현재 위치가 redirect했던 위치이므로 response를 /
페이지로 변경을 다시 하던지 아니면 위에처럼 쿠키 path를 루트로 잡아줘야 다른 모든 경로에서 사용이 가능합니다.
이제 값을 받아올 차례입니다.
이번엔 먼저, 쿠키 값을 가져와야 합니다. 쿠키를 가져온 후 userinfo와 맞는 url로 요청을 주게되면
@GetMapping("/info")
public ResponseEntity token(HttpServletRequest request) {
String accessToken = Arrays.stream(request.getCookies())
.filter(cookie -> cookie.getName().equals("Authorization"))
.map(Cookie::getValue)
.findFirst()
.orElseGet(null);
StringBuilder re = new StringBuilder("https://www.googleapis.com/oauth2/v2/userinfo?");
re.append("access_token=").append(accessToken);
return new RestTemplate().exchange(
re.toString(),
HttpMethod.GET,
null,
String.class
);
}

와 같이 값이 제대로 나오는 것을 확인할 수 있습니다.
OAuth를 구현하면서 생각해봤던 간단한 문제들
1. Resource Owner와 Client와 Authentication Server,Resource Server의 관계
사실 이 부분은 간단하다. OAuth는 Resource Owner와 Resource Server간 인증과 인가를 위한 프로토콜이고 Client와는 상관이 없다.
2. 내가 구현하고자 하는 서비스가 뭔가
제가 구현하고자 했던 어플은 OAuth Server의 API가 거의 필요 없는 어플리케이션이었습니다.
먼저 반대의 경우부터 생각해보겠습니다. 'OAuth Server API가 지속적으로 필요한 경우라면 어떻게 구현해야 했을까요?'
간단하게 생각하면 OAuth Server에 지속적인 access Token을 재발급 받으면 될 거 같습니다.
그럼, 'OAuth Server APi가 가끔 필요한 경우라면?'
이 경우도 OAuth2.0부터는 Refresh Token이 존재하여 상당히 긴 주기로 가져갈 수 있습니다. 그렇기 때문에 이 부분도 어떻게 보면 문제가 없습니다.
그렇다면 '내가 구현하고 싶은 거의 필요없는 아니면 아예 필요 없을 수도있는 어플리케이션이라면?'
OAuth는 회원가입과 처음 로그인했을 때만 필요하고 그 이후엔 로그인을 유지할 수 없으니까 우리만의 로그인이 필요하게 됩니다. OAuth2를 기반으로한 프로토콜에서는 현재 OAuth Server의 Access Token을 저장하여 값을 전달해 주지만 그럴 필요가 없고, 로그인이나 회원가입과 함께 Access Token과 Refresh Token을 만들어 Resource Owner와 Client간 인증과 인가 절차를 구현해 놓으면 되기 때문입니다.
3. OAuth2와 OAuth1그리고 OAuth2.1에서의 Token
OAuth1.0에서는 Access Token을 사용하기에는 보안으로 인해 짧은 유효기간을 넣을 수 밖에 없었습니다. 이를 개선하여 OAuth2.0에서 부터는 Access Token에 Refresh Token을 통해 사용자 경험을 향상시키려고 하였습니다.
하지만 이렇게 하더라도 Refresh Token에는 기간이 있기 때문에 그 기간내에만 가능하다는 단점이 있었습니다.(이후 여러 단점이 있긴 하지만, 이전 JWT에서 다뤘습니다.)
이런 문제를 Rotation으로 해결할 수 있었는데, 아직 구현되지 않은 OAuth2.1에서 해당 Token Rotation을 사용한다고 합니다. Rotation 토큰이 도입이 안된 OAuth2.0의 경우 Refresh Token의 만료를 Client Server에서 해결할 수 있을까에 대해서도 고민해봤는데 안된다는 쪽으로 거의 기울었네요.
마지막으로 Spring security를 통한 구현도 추가해 놓겠습니다!
- OAuth2란?
- OAuth2가 어떤 프로토콜이길래 가능한 거지?
- OAuth2의 구성
- Client 등록
- OAuth2의 동작
- 1. 사용자의 로그인
- 2. Redirect URI with Authorization code & Get Access Token
- 3. Resource 받기
- OAuth를 구현하면서 생각해봤던 간단한 문제들
- 1. Resource Owner와 Client와 Authentication Server,Resource Server의 관계
- 2. 내가 구현하고자 하는 서비스가 뭔가
- 3. OAuth2와 OAuth1그리고 OAuth2.1에서의 Token