brunch

You can make anything
by writing

C.S.Lewis

by 이수홍 Feb 26. 2017

Spring Boot로 만드는 OAuth2 시스템 9

OAuth2 시스템에서 Scope를 이용한 API 권한 제어

이전 챕터에서는 클라이언트 정보를 관리하는 부분을 확인해보았다.

이번에는 scope를 구체적으로 관리하는 방법을 알아보자. 먼저 scope는 앞서서도 잠깐 살펴보았지만 클라이언트가 API에 접속할 수 권한의 범위를 제한할 때 이용한다. 

페이스북 scope 체크

페이스북의 예를 들어 보자면 서드파티 클라이언트(페이스북 용어는 앱이다.)를 통해서 페이스북에 접속하게 되면 위와 같이 클라이언트(앱)가 접속 권한을 체크하게 되면서 위와 같이 묻게 된다. 위에서 사용자에게 허가를 받아야 클라이언트(앱)가 페이스 API에 접속할 수 있는 권한을 받게 되는 것이다.

위 이미지에서 클라이언트에서 사용자에게 허가를 요구하는 스코프가 바로 공개 프로필, 이메일 주소인 것이다.

그래서 이전 챕터에서 이야기 한 클라이언트에서 scope 속성이 하는 역할이 바로 그런 부분이다. 즉 클라이언트의 scope에 따라 API 접근 범위를 달리할 수 있게 된다. 보통 말하는 권한이라고 생각하면 된다.

페이스북 API 권한(Scope)

위 그림과 같이 페이스북 로그인 관련 API에서 접근 가능한 권한 리스트를 보여주고 있다. 
(페이스북 API 권한(scope) 링크)

이제 scope에 대해서 조금이라도 감이 잡히시는가? 직접 어떻게 구현되어서 사용되고 있는지 보고 나서 이해해보자.


scope를 제대로 사용하기 위해서는 권한에 따른 접근제어가 필요한 부분에 대해서 여러 가지 경우를 나열 해보아야한다.
그 부분이  접근 제어가 필요한 API가 될 수도 있고 필터링된 데이터가 될 수도 있다.
(scope별 API를 접근 자체를 막거나 같은 API라도 scope에 따라서 다른 정보를 전달 할 수 있다.)

scope 문자열은 개발자와 기획자 등 관련 결정자가 어떻게 결정하는 것에 따라 다르기 때문인데 정답은 존재하지 않는다. 단지 조금 더 명시성, 효율성, 유연성 등을 고려한 형태가 존재할 뿐이다. 

위의 캡처된 이미지에 보이는 페이스북 권한 부분 참고해서 간단하게 정리를 해보았다.

member.info.public: 기본 공개된 회원 정보 조회 scope 
member.info.email: 회원 이메일 주소
member.info.phone: 회원 휴대폰 번호
member.info.nick: 회원 닉네임
member.info.last_login_date: 회원 마지막 로그인 일자 정보
order.today.list: 오늘 주문 내역 리스트
...

자 위와 같이 정의된 scope가 있으면 이제 클라이언트에서는 위 권한에서 어떤 정보가 필요한지 알아야 한다.

보통 클라이언트는 현재 시스템과 완전 다른 시스템이라는 생각을 해야 한다.

이전 챕터에서 클라이언트 관리에 대해서 이야기했었는데 클라이언트 기술하는 부분을 다시 살펴보자.

//직접 기술하는 부분
.scopes("read", "write")
//...

위와 같이 클라이언트 정보를 직접 기술하는 부분에서 (DB에서는 scope칼럼과 동일) 이전까지 테스트에서는  read, write라는 형태로 막연히 적혀 있는 있었을 것을 확인할 수 있을 것이다.

하지만 이제 위처럼 만들어진 scope를 시스템에 적용시켜 보자.

// ...
.withClient("my_client_id")
// ...
// 이 클라이언트는 기본 회원정보와, 이메일 주소를 제공하는 scope가 필요하다. 
.scopes("member.info.public", "member.info.email")
// ...
.withClient("your_client_id")
// 이 클라이언트는  기본 회원정보와, 휴대폰 번호를 제공하는 scope가 필요하다. 
.scopes("member.info.public", "member.info.phone")

DB로 관리한다면 위와 동일하게 수정해주면 된다.

insert into oauth_client_details (client_id, client_secret, resource_ids, scope, authorized_grant_types, web_server_redirect_uri, authorities, access_token_validity, refresh_token_validity, additional_information, autoapprove) values 
('my_client_id', 'my_client_secret', null, 'member.info.public,member.info.email', 'authorization_code,password,client_credentials,implicit,refresh_token', null, 'ROLE_MY_CLIENT', 36000, 2592000, null, null);
---
insert into oauth_client_details (client_id, client_secret, resource_ids, scope, authorized_grant_types, web_server_redirect_uri, authorities, access_token_validity, refresh_token_validity, additional_information, autoapprove) values 
('your_client_id', 'your_client_secret', null, 'member.info.public,member.info.phone', 'authorization_code,implicit', null, 'ROLE_YOUR_CLIENT', 36000, 2592000, null, null);

이제 scope에 따라 회원 정보를 얻어 오는 API를 만들어보자.


기존 소스에서 직접 회원 정보를 얻어와야 하기 때문에 관련 API를 직접 만들어볼 예정이다.

아래는 관련 소스의 일부분이다. 

@RestController
class MemberContoller {
    private static final Logger log = LoggerFactory.getLogger(MemberContoller.class);

    @Autowired
    MemberRepository memberRepository;

    @PreAuthorize("#oauth2.hasScope('member.info.public')")
    @RequestMapping("/api/member")
    public MemberData member(@AuthenticationPrincipal OAuth2Authentication authentication) {
        String username = authentication.getUserAuthentication().getPrincipal().toString();
        Set<String> scopes = authentication.getOAuth2Request().getScope();
        log.info("Member's username = {}", username);
        log.info("Client scope info = {}", scopes);
        Member member = memberRepository.findByUsername(username);
        return MemberData.from(member,scopes);
    }
//...

위 소스에서 주목해야 하는 부분 아래와 같다. 

// 이 API에 접근하기 위해서는 "member.info.public" 이 scope가 필요하다.
@PreAuthorize("#oauth2.hasScope('member.info.public')")

이 부분과 

// 발급된 액세스 토큰이 가지고 있는 scope 리스트를 가져온다.
Set<String> scopes = authentication.getOAuth2Request().getScope();

이 부분이다. 

액세스 토큰 발급 시에 scope를 같이 가져오는 것을 보았을 것이다.

아래는 "자원 소유자 비밀번호" 방식으로 지정된 scope를 가지는 액세스 토큰을 발급하는 방법이다.

$ curl my_client_id:my_client_secret@localhost:8080/oauth/token -d grant_type=password -d client_id=my_client_id -d scope=member.info.public -d username=user -d password=test

위 방법에서 "scope=member.info.public" 이 부분에 주목해야 한다. 이렇게 발급된 액세스 토큰은 "member.info.public"의  scope만 가지기 때문에 이 범위에 해당하는 API 또는 해당 정보를 가질 수 있게 되는 것이다. ( 두 개 이상의 scope는 스페이스로 구분한다. RFC6749 )

그리고 반드시 클라이언트 발급 시 가졌던 scope만 입력 가능하다. 만약에 클라이언트가 가지고 있지 않는 scope를 발급하게 되면 다음 형태의 오류가 발생할 것이다. ( RFC6749 )

{
 "error": "invalid_scope",
  "error_description": "Invalid scope: member.info.public",
  "scope": "read write" /*현재 클라이언트에서 허용하는 scope*/
}

위에서 이야기한 것에 대해서 정리해보겠다.
- scope는 클라이언트와 액세스 토큰에 대한 권한을 결정한다.
- 클라이언트 등록 시 필요한 scope를 정의해서 가진다.
- 액세스 토큰 발급 시에는 클라이언트가 가지는 scope 범위에서만 선택할 수 있다. 

- 액세스 토큰 발급 시 scope에 대해서 사용자의 동의를 얻을 시에는 사용자가 특정 scope를 거부하게 되면 그 토큰은 거부된 scope를 제외하고 토큰이 발급된다. (즉 애플리케이션에서는 필요하지만 거부된 scope에 대한 부분에 대해서 다른 방법을 모색해야 한다.)  
- 액세스 토큰을 가지고 API 호출 시 토큰이 가지고 있는 scope(클라이언트가 가지고 있는 scope 전체가 아님)만큼의 권한을 가지고 호출 가능하다. 

- scope를 이용해서 애플리케이션을 제어하는 방법을 따로 정해진 방법은 없다. 설계에 따라 API 접근 권한이 될 수도 있고 데이터의 필터 여부를 결정할 수도 있다. (즉 개발하기 나름이라는 뜻)


위 소스는 여기서 확인 가능하다. 


이전 포스팅: 8. OAuth2 서버를 커스터마이징 해보자(클라이언트 관리 편)

1. 스프링 부트와 OAuth2 (https://brunch.co.kr/@sbcoba/1 )  
2. 본격적인 개발 하기 전에 (https://brunch.co.kr/@sbcoba/2 )
3. API 서버 만들기 (https://brunch.co.kr/@sbcoba/3 )  
4. 간단한 OAuth2 서버 만들어 보기 (https://brunch.co.kr/@sbcoba/4 )  
5. OAuth2 서버를 커스터마이징 해보자(TokenStore 편) (https://brunch.co.kr/@sbcoba/5 )  
6. API 서버와 OAuth2 서버를 분리 (https://brunch.co.kr/@sbcoba/6 )
7. JWT 방식으로 바꿔 보자 (https://brunch.co.kr/@sbcoba/7 )
8. OAuth2 서버를 커스터마이징 해보자(클라이언트 관리 편) (https://brunch.co.kr/@sbcoba/8 )
9. OAuth2 시스템에서 Scope를 이용한 API 권한 제어 (https://brunch.co.kr/@sbcoba/15 )
매거진의 이전글 Spring Boot로 만드는 OAuth2 시스템 8
브런치는 최신 브라우저에 최적화 되어있습니다. IE chrome safari