OAuth

2019-08-21
  • web
  • OAuth 를 알아보기 전에 용어 정리가 필요하다.

    • Authentication : 인증, 내맘대로 해석 - 요청 대상이 예상하는 대상이 맞는지 확인하는 것.

      컴퓨터 보안에서 인증은 로그인 요청 등을 통해 통신 상에서 보내는 사람의 디지털 정체성을 확인하는 시도의 과정이다. 출처

    • Authorization : 허가, 인가, 내맘대로 해석- 요청 자원에 대한 접근 허가

      허가(허용; authorization)란 리소스에 대한 접근 권한 및 정책을 지정하는 기능이다. 정보 보안 및 컴퓨터 보안, 특히 접근 제어 분야와 관련이 있다.[1] 예를 들어 인사부서 직원들은 보통 직원 데이터를 열람할 수 있도록 허용되어 있는데, 이러한 정책은 일반적으로 컴퓨터 시스템에 접근 제어 규칙들로 저장된다. 컴퓨터 시스템은 어떤 (인증된) 리소스 수요자가 리소스에 대한 요청을 하면, 저장된 접근 제어 규칙들을 적용해 요청을 허가할지 거부할지를 결정한다. 여기서 리소스는 개개의 파일 또는 데이터, 컴퓨터 프로그램이나 프로그램의 일부 기능, 컴퓨터 하드웨어 및 장치 등을 포함한다. 그리고 리소스 수요자는 컴퓨터 사용자뿐만 아니라 컴퓨터 프로그램이나 다른 장치들이 될 수도 있다. 출처

    Request for Comments: 6749, RFC 6749

    OAuth 1.0a

    1.0 이 기존에 있었지만, 보안이 수정된 1.0 revision A 이 표준 RFC5849 로 등록이 되었다고하니 1.0 revision A 만 살짝 맛보고 가면 되겠다. 지금 사용하는건 대부분 2.0 이니까..

    용어

    용어 설명
    User Service Provider 에 계정을 가지고 있으면서, Consumer 를 이용하려는 사용자
    Consumer OAuth 를 사용하는 Open API 를 제공하는 서비스
    Service Provider OAuth 인증을 사용해 Service Provider 의 기능을 사용하려는 애플리케이션이나 웹 서비스
    Request Token Consumer가 Service Provider 에게 접근 권한을 인증받기 위해 사용하는 값. 인증이 완료된 후에는 Access Token 으로 교환.
    Access Token 인증 후 Consumer 가 Service Provider 의 자원에 접근하기 위한 키를 포함한 값

    Flow

    OAuth1.0 flow

    OAuth 2.0 을 먼저 배웠고, 어떤 형태를 거쳐 여기까지 왔는지 궁금하여 검색해 본 것인데, Flow 를 대충 보고 참고자료를 보며 느낀 점은 음? 별로 크게 다르지 않은 것 같네? 였다. 웹애플리케이션이 아닌 일반 애플리케이션에 대한 지원강화, HTTPS 사용으로 보안알고리즘 사용 자제, Access Token 의 만료적용. 이 정도가 크게 달라진 점 같다. 아 그냥 이랬구나 하고 넘어가련다.

    OAuth 2.0

    용어

    RFC 상에는 5가지 방식이 있다.

    1. Authorization Code Grant - 아래에서 설명
    2. Implicit Grant
      • 암시적 승인 - 보통 javascript 로 사용
      • user-agent(보통 브라우저) 에서 권한(Authorization)요청과 토큰요청이 분리되어 나감.
      • client 의 인증(authentication) 이 없다.
      • access token 이 redirection URI 에 노출된다.
      • refresh token 이 없다.
    3. Resource Owner Password Credentials Grant
      • resource owner(유저) 의 비밀번호를 app 에서 저장하는 방법
    4. Client Credentials Grant
      • app(client) 의 자격만 가지고 토큰을 발급받는 방법
      • app 을 신뢰하기 때문에 다른 인증 필요 없이 바로 토큰을 발급해준다.
      • 보통은 Authorization Server 와 Client 가 같은 주체일 때 그런듯
    5. Extension Grants
      • 이건 그냥 알아서 해라 같은데.. RFC 에도 딱히 뭐가 없네

    Authorization Code Grant

    개인적으로 가장 보편적이라 생각하는 Authorization Code Grant 방식에 대해서 설명한다.

    OAuth2.0 Flow

    from slack api doc

    Flow 를 보면 알 수 도 있지만, 간략하게 순서를 정리해 보자면 (Web Application 기준)

    1. OAuth 2.0 의 방식으로 API Service 를 제공하는 서비스에 가입하여 본인이 개발할 Web Application 을 등록한다.
    2. Client ID, Client Secret 을 발급 받고, 본인이 개발할 Web ApplicationCallback URL 을 등록한다.
    3. Access TokenOAuth 2.0 Flow 에 따라서 발급받는 Web Application 기능을 추가한다.
    4. OAuth 2.0 Flow 를 따라 Web Application 에서 Acess Token 을 발급 받는다.
    5. Acess Token 으로 Resource 에 접근한다.

    Naver, Facebook, Instagram, Google+ 등과 라이브러리 없이 연동을 해 보았다. 약간씩 파라미터가 다르지만 대동소이하다. 예제는 Naver.

    설정 값을 저장할 Constants class.

    package util;
    
    import io.netty.handler.codec.http.HttpMethod;
    
    public class Constants {
    	public enum OAUTH2_CLIENT {
    		NAVER(
    			/* auth uri     */ "https://nid.naver.com/oauth2.0/authorize"
    			/* auth method  */, HttpMethod.POST
    			/* token uri    */, "https://nid.naver.com/oauth2.0/token"
    			/* token method */, HttpMethod.POST
    			/* resource uri           */, "https://openapi.naver.com/v1/nid/me"
    			/* resource method        */, HttpMethod.POST
    			/* resource header keys   */, new String[] {"Authorization"}
    			/* resource header values */, new String[] {"Bearer "}
    			/* client id     */, "MY_CLIENT_ID"
    			/* client secret */, "MY_CLIENT_SECRET"
    			/* callback uri  */, "MY_CALLBACK_URL"
    		),
    		;
    		private String authUri;
    		private HttpMethod authPreferedMethod;
    		private String tokenUri;
    		private HttpMethod tokenPreferedMethod;
    		private String resourceServerUrl;
    		private HttpMethod resourceServerPreferedMethod;
    		private String[] resourceHeaderKey;
    		private String[] resourceHeaderValue;
    		private String clientId;
    		private String clientSecret;
    		private String callbackUri;
    		public String getAuthUri() { return this.authUri; }
    		public HttpMethod getAuthPreferedMethod() { return this.authPreferedMethod; }
    		public String getTokenUri() { return this.tokenUri; }
    		public HttpMethod getTokenPreferedMethod() { return this.tokenPreferedMethod; }
    		public String getResourceServerUrl() { return this.resourceServerUrl; }
    		public HttpMethod getResourceServerPreferedMethod() { return this.resourceServerPreferedMethod; }
    		public String[] getResourceHeaderKeys() { return this.resourceHeaderKey; }
    		public String[] getResourceHeaderValues() { return this.resourceHeaderValue; }
    		public String getClientId() {return this.clientId; }
    		public String getClientSecret() { return this.clientSecret; }
    		public String getCallbackUri() { return this.callbackUri; }
    		@Override
    		public String toString() { return "DO NOT USE DEFAULT TOSTRING"; }
    		private OAUTH2_CLIENT(
    				String authUri
    				, HttpMethod authPreferedMethod
    				, String tokenUri
    				, HttpMethod tokenPreferedMethod
    				, String resourceServerUrl
    				, HttpMethod resourceServerPreferedMethod
    				, String[] resourceHeaderKey
    				, String[] resourceHeaderValue
    				, String clientId
    				, String clientSecret
    				, String callbackUri
    		) {
    			this.authUri = authUri;
    			this.authPreferedMethod = authPreferedMethod;
    
    			this.tokenUri = tokenUri;
    			this.tokenPreferedMethod = tokenPreferedMethod;
    
    			this.resourceServerUrl = resourceServerUrl;
    			this.resourceServerPreferedMethod = resourceServerPreferedMethod;
    			this.resourceHeaderKey = resourceHeaderKey;
    			this.resourceHeaderValue = resourceHeaderValue;
    
    			this.clientId = clientId;
    			this.clientSecret = clientSecret;
    			this.callbackUri = callbackUri;
    		}
    	}//END OF ENUM
    }//END OF CLASS
    

    인증과 인가를 거쳐 Access Token 발급을 위한 TestAuthFacebookController class

    package controller;
    
    import java.io.UnsupportedEncodingException;
    import java.math.BigInteger;
    import java.net.URLEncoder;
    import java.nio.charset.Charset;
    import java.security.SecureRandom;
    import java.util.Map;
    
    import javax.servlet.http.HttpServletResponse;
    
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestMethod;
    import org.springframework.web.bind.annotation.RequestParam;
    import org.springframework.web.servlet.ModelAndView;
    
    import com.fasterxml.jackson.databind.ObjectMapper;
    
    import util.Constants.OAUTH2_CLIENT;
    import util.HttpUtils;
    
    @Controller
    public class TestAuthFacebookController {
    
    	private static final Logger logger = LoggerFactory.getLogger(TestAuthFacebookController.class);
    	public static String storedState = "";
    
    	@RequestMapping(value="oauth/facebook/init", method=RequestMethod.GET)
    	public ModelAndView oAuth2TestFacebookInit(HttpServletResponse httpServletResponse) throws UnsupportedEncodingException {
    	    String stateToken = new BigInteger(130, new SecureRandom()).toString(32);
    	    logger.debug("stateToken : " + stateToken);
    	    TestAuthFacebookController.storedState = stateToken;
    	    String redirectUrl = OAUTH2_CLIENT.FACEBOOK.getAuthUri()
    	    		+ "?client_id=" + OAUTH2_CLIENT.FACEBOOK.getClientId()
    	    		+ "&response_type=code"
    	    		+ "&redirect_uri=" + URLEncoder.encode(OAUTH2_CLIENT.FACEBOOK.getCallbackUri(), Charset.forName("UTF-8").toString())
    	    		+ "&state=" + stateToken
    	    		+ "&scope=public_profile email manage_pages";
    	    return new ModelAndView("redirect:" + redirectUrl);
    	}//END OF FUNCTION
    
    	@RequestMapping(value="oauth/facebook/callback", method=RequestMethod.GET)
    	public String oAuth2TestFacebookCallback(@RequestParam Map<String,Object> map) throws Exception {
    		String recvState = (String) map.get("state");
    		logger.debug("recv state : " + recvState);
    		String recvCode = (String) map.get("code");
    		logger.debug("oAuth2TestFacebookCallback recv code : " + recvCode);
    		if(TestAuthFacebookController.storedState.equals(recvState)) {
    			String paramStr = ""
    				+ "client_id=" + OAUTH2_CLIENT.FACEBOOK.getClientId()
    				+ "&client_secret=" + OAUTH2_CLIENT.FACEBOOK.getClientSecret()
    				+ "&code=" + recvCode
    				+ "&redirect_uri=" + OAUTH2_CLIENT.FACEBOOK.getCallbackUri();
    
    			ObjectMapper mapper = new ObjectMapper();
    			String jsonInString = HttpUtils.sendAndRecv(
    				OAUTH2_CLIENT.FACEBOOK.getTokenUri()
    				, OAUTH2_CLIENT.FACEBOOK.getTokenPreferedMethod()
    				, paramStr
    			);
    
    			logger.debug("AuthorizationServer recv : " + jsonInString);
    
    		} else {
    			throw new Exception("");
    			//401 unauthorized
    		}
    		return "main";
    	}//END OF FUNCTION
    
    }//END OF CLASS
    
    

    참고

    Facebook API - 로그인 플로 직접 빌드

    공식문서

    로그인 대화 상자 호출 및 리디렉션 URL 설정

    클라이언트 앱(서버)에서 페이스북 로그인 화면으로 리디렉션 한다.
    로그인 화면에서 사용자(Resource Owner)가 응답을 했을 경우, 되돌아올 클라이언트 앱의 리디렉션 URL (페이스북 앱 관리 페이지에서 미리 설정된 URL 과 같아야함) 과 CSRF 를 위한 문자열 state 을 설정해 준다.

    https://www.facebook.com/v4.0/dialog/oauth?
      client_id={app-id}
      &redirect_uri={redirect-uri}
      &state={state-param}
    
    • client_id : 페이스북 앱 관리 페이지에서 등록한 클라이언트 앱의 발급받은 ID
    • redirect_uri : 사용자가 로그인 후, 권한승인을 했을 때 리디렉션될 클라이언트 앱의 URI
    • state : 아래 네 단계의 flow 를 거치면서 위변조를 막기위해 클라이언트 앱이 1 단계에서 만들어서 4. 단계에서 돌려 받는 문자열.
      1. 클라이언트 앱
      2. 사용자 로그인
      3. 권한서버
      4. 클라이언트 앱
    • response_type : 클라이언트 앱으로 리디렉션할 때 포함된 응답 데이터가 URL 매개변수인지 아니면 프래그먼트인지 결정하는 옵션
      • code : 응답 데이터는 URL 매개변수, 기본값
      • … 뭐가 더 있음. 하지만 관심이 없으므로 생략.

    그럼 사용자는 페이스북 로그인 화면을 보게되고, 로그인 후 해당 클라이언트 앱에서 사용할 수 있는 권한을 선택한 뒤, 승인 여부를 선택한다. 그러면 기존에 설정된 클라이언트 앱의 리디렉션 URL 을 호출해 준다.

    로그인 대화 상자 응답 처리

    위에서 response_typecode 로 했기 때문에, access token 과 교환 할 수 있는 code 와 클라이언트 앱에서 생성한 state 를 돌려 받는다. state 가 생성했던 값과 같은지 확인 한 다음, codeaccess token 으로 교환한다.

    GET https://graph.facebook.com/v4.0/oauth/access_token?
       client_id={app-id}
       &redirect_uri={redirect-uri}
       &client_secret={app-secret}
       &code={code-parameter}
    
    • client_id : 페이스북 앱 관리 페이지에서 등록한 클라이언트 앱의 발급받은 ID
    • redirect_uri : OAuth 로그인 프로세스를 시작할 때 사용한 원래 request_uri
    • client_secret : 페이스북 앱 관리 페이지에서 등록한 클라이언트 앱의 발급받은 Secret
    • code : 전달 받은 code

    위와 같이 요청을 하면, 아래와 같이 응답을 받을 수 있다.

    {
      "access_token": {access-token},
      "token_type": {type},
      "expires_in":  {seconds-til-expiration}
    }