spring/spring security

스프링 시큐리티 기본 개념공부 11. (Oauth2 실습)

대기업 가고 싶은 공돌이 2024. 7. 18. 21:36

Section 13.

KeyCloak

  • 키클락이란 인증 서버를 제공해주는 서비스로 바로 사용할 수 있는 구축된 서버를 제공한다.
  • 키클락 외에도 Okta, ForgeRock, Amazon Congnito등에서 인증 서버를 제공해준다.
  • 키클락은 오픈소스로서 비용이 들지 않음에도 불구하고 안정적이며 주기적인 업데이트가 이뤄진다.
  • 외에도, 액세스 토큰 발급, SSO 기능(하나의 인증서버를 통해 다른 모든 어플리케이션에 접근 가능하도록 하는 기능) 제공, 소셜 로그인 기능, 중앙 제어 기능 등등을 제공한다.

KeyCloak 설치 방법

  • 사이트에서 파일을 다운받은 뒤 터미널에서 bin/kc.bat start-dev로 실행하면 된다.
  • 이제 port번호 8080 으로 실행될 것이며 localhost 8080으로 접속하면 관리자 계정을 생성하고 여러가지 권한을 수정할 수 있다.

Keycloak의 여러가지 권한

  • 우선 keycloak에 로그인하면 Master realm이란 페이지가 나온다. 여기서 realm이란 인증 서버 내의 공간을 의미한다. 해당 공간에서 user, role, client를 생성할 수 있다.
  • Master realm은 모든 어플리케이션을 관리하는 공간이다. 하지만 모든 어플리케이션을 한 곳에서 관리하면 확장성이 매우 떨어진다. 따라서 각각의 어플리케이션마다 realm을 따로 만들어 어플리케이션마다 다른 권한과 역할을 부여해주는것이 효율적이다.
  • 따라서 우리 어플리케이션에 적합한 새로운 realm을 생성해야한다.
  • 새로운 realm을 생성했으면 내부에서 클라이언트, 역할, 사용자, 그룹등을 원하는만큼 만들 수 있다.
  • 위의 방법들을 따라하면 인증 서버를 성공적으로 구축할 수 있다.

client credentials grant type flow의 Api간 소통 시나리오

  • client credentials grant type flow로 엔드유저가 연관돼있지 않은 시나리오다.

인증 서버에 client 등록하기

  • 우선 클라이언트 어플리케이션에서 인증 서버에 보내줄 client 정보를 받아야한다. 그러므로 keycloak 인증 서버에 클라이언트를 등록해보자.
  • 우선 eazybank realm에 들어가고 client 메뉴에 들어가면 원하는 만큼 client를 등록할 수 있다. !(키클락)[키클락예시.png]
  • 이 시나리오에선 우리가 직접 등록을 하지만 실제에서는 서드 파티에서 요청을 하고 내부 로직에서 타당한지 검사한 후에 keycloak에 클라이언트 등록을 해주는 방식으로 동작해야할것이다.
  • 클라이언트 생성을 해보자 먼저 oauth2인지 openid connect 인지 설정해주고 그 다음엔 클라이언트 secret을 통한 인증을 진행할 것인지 정해줘야한다. 이후 인증 유형을 정해줘야한다. !(클라이언트)[클라이언트예시.png]
  • 위에서 standard flow는 authentication code flow를 의미하므로 해제 해준다. 우리는 client credentials grant type을 사용할 것이니 Service accounts roles를 선택해준다.
  • 이제 저장을 해주면 클라이언트 secret을 확인할 수 있다.

리소스 서버 구축하기

  • 우선 백엔드 어플리케이션을 리소스 서버로 동작하게 만들기 위해선 다음과 같은 의존성이 필요하다.
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
		</dependency>
  • 위 의존성을 추가하면 백엔드 어플리케이션은 리소스 서버처럼 행동하며, api를 요청하는 유저에게 액세스 토큰을 요청하게 된다.
  • 이제 액세스 토큰에서 유저의 정보를 추추하여 백엔드에서 이해할 수 있는 형식으로 바꿔줄 클래스를 만든다.
public class KeycloakRoleConverter  implements Converter<Jwt, Collection<GrantedAuthority>> {
  • 클래스는 다음과 같이 jwt 토큰을 GrantedAuthority 컬렉션으로 반환하는 Converter를 구현해야한다.
  • 해당 인터페이스는 다음과 같은 메소드를 오버라이드 해야한다.
@Override
    public Collection<GrantedAuthority> convert(Jwt jwt) {
        Map<String, Object> realmAccess = (Map<String, Object>) jwt.getClaims().get("realm_access");

        if (realmAccess == null || realmAccess.isEmpty()) {
            return new ArrayList<>();
        }

        Collection<GrantedAuthority> returnValue = ((List<String>) realmAccess.get("roles"))
                .stream().map(roleName -> "ROLE_" + roleName)
                .map(SimpleGrantedAuthority::new)
                .collect(Collectors.toList());

        return returnValue;
    }
  • jwt.getClaims를 통해 이름이 realm_access인 Map을 가져온다.
  • 이후 이름이 roles인 모든 객체를 반환시켜 리스트로 만들어 반환한다.
  • 이제 defaultSecurityFilterchain 내부에 다음과 같은 코드를 통하여 우리가 만든 컨버터를 인식할 수 있게 만들어줘야한다.
JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
        jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(new KeycloakRoleConverter());
  • 이제 아래와 같은 코드로 리소스 서버의 기능을 수행하도록 설정해준다.
.requestMatchers("/user").authenticated()
                        .requestMatchers("/notices","/contact","/register").permitAll())
                .oauth2ResourceServer(oauth2ResourceServerCustomizer ->
                        oauth2ResourceServerCustomizer.jwt(jwtCustomizer -> jwtCustomizer.jwtAuthenticationConverter(jwtAuthenticationConverter)));
  • 아까 만들어둔 jwtAuthenticationConverter도 등록해주며 jwt 액세스 토큰을 활용할 것이라고 알려준다.
  • 리소스 서버의 api는 모든 입력 파라미터를 id 에서 email로 바꿔야한다. 왜냐하면 서드 파티 어플리케이션에서는 절대로 해당 유저의 id를 알 수 없기 때문이다.
  • 이제 application.properties에 인증 서버의 url을 남겨줌으로서 인증서버에서 액세스 토큰과 관련된 인증서를 다운받아 액세스 토큰을 검증할 수 있다.
    • 인증서에는 키 값이 저장돼 있고 해당 키 값을 통해 서명 부분을 해석하여 토큰이 조작되지 않았는지도 리소스 서버에서 확인이 가능하다.

인증 서버에서 액세스 토큰 받기

"realm_access": {
    "roles": [
      "offline_access",
      "uma_authorization",
      "default-roles-eazybankdev"
    ]
  },
  • 다음과 같은 realm_access에 roles가 잇는 것을 볼 수 있다. 저 roles를 통해 역할을 추출할 수 있는것이다.
  • 그런데 여기엔 문제가 있다. 바로 위의 역할은 우리가 설정한 역할과는 다르다는 것이다.
  • 현재는 우리가 인증서버의 관리자이므로 keyCloak에서 역할을 전부 우리의 형식에 맞게 바꿔주면 된다.
  • 그러나 페이스북이나 구글 등을 사용할 때는 해당 소셜에서 건네주는 역할을 사용하는 방식으로 하거나 우리가 설정한 역할을 따로 추가해주는 방식으로 해야할 것이다.
  • 받은 액세스 토큰을 리소스 서버에 보내려면 Authoriztion 헤더를 만들어 값을 "Bearer" +" " + "액세스토큰" 형식으로 보내면 된다.

authorization code grant type flow의 시나리오

  • 자바 스크립트 어플리케이션은 코드가 모두 공개되어있어 client secret을 보내는 것이 불가능하다.
  • 현재 시나리오는 ui 어플리케이션이 자바 스크립트가 아니라는 가정하에 이뤄진다.

인증서버에 클라이언트 등록하기

  1. 위에서 했던 것과 마찬가지로 Keycloak에 클라이언트를 새로 등록해준다. 이번 클라이언트는 authorization code grant flow를 사용한다.
  2. valid redirect URLs에 인증에 성공한 사용자를 되돌려줄 url을 적어준다.
  3. authorization code grant flow는 실사용자가 필요하기에 users 탭에 사용자 등록을 해준다.

인증서버에 인증 코드 요청하기

  1. client_id
  2. response_type : code
  3. scope : openid
  4. redirect_uri : ~~~~
  5. state_value : csrf 토큰 값
  • 받은 인증 코드 값을 바탕으로 post 요청을 보내서 액세스 토큰을 받아야한다.
  • 해당 요청은 post 요청이기 때문에 body의 urlendoded를 활용하여 정보를 보내자.
  • 넘겨야 할 정보는 다음과 같다
  1. client_id
  2. client_secret
  3. grant_type : authorization_code
  4. code : 받은 인증 코드
  5. redirect_uri : ~~~
  6. scope : openid
  • 해당 정보를 보내면 액세스 토큰을 받을 수 있다.

proof Key for Code Exchange flow

  • PKCE flow를 따르면 자바스크립트 기반의 ui에서도 authorization code grant flow를 활용할 수 있다.
  • 이 플로우에서 엔드유저가 인증에 성공하고 원래 페이지로 redirect 될 때마다 클라이언트 어플리케이션에서는 code_varifier를 생성한다.
  • 그리고 code_varifier로부터 code_challenge를 생성한다.
  • code_challenge를 생성하기 위해 sha256 알고리즘을 무작위로 생성된 code_varifier를 활용하여 실행시킨다.
  • 해시 string이 생성됐다면 해당 값을 클라이언트 어플리케이션은 base64-URL로 인코딩한다. 이렇게 base64-URL로 인코딩 된 값을 code_challenge라고 한다.
  • 인증 코드를 받으려 하고 있으며 아직 액세스 토큰은 요청중이 아닐 때 code_challenge를 인증 서버에 보낸다.
  • 그러면 인증 서버는 code_challenge를 저장소에 보관하고 클라이언트 어플리케이션에게는 code_challenge에 대응하는 인증코드를 보낸다.
  • 이제 클라이언트 어플리케이션이 액세스 토큰을 받으려 할때 인증 서버에 code_varifier와 아까 받은 인증 코드를 둘 다 보낸다.
  • 인증서버는 code_varifier를 sha256 알고리즘에 넣어 해시 값을 얻는다. 얻은 값을 이전에 저장해두었던 값과 비교하고, 인증 코드도 비교한다. 두 값이 모두 일치해야지만 액세스 토큰을 반환한다.
  • 인증 코드를 탈취당했다 하더라도 클라이어늩 어플리케이션에만 저장된 code_varifier를 알진 못하기 때문에 보안을 강화할 수 있다.

PKCE 요약

  1. 사용자가 리소스 접근 요청을 한다.
  2. 클라이언트가 인증 서버 로그인 페이지로 보낼테니 로그인을 하라고 한다.
  3. 사용자를 인증서버로 보내는 것과 동시에 클라이언트는 code_challenge 값과 client id 값을 인증 서버에게 공유한다.
  4. 인증 서버는 저장소에 code challenge를 저장한다.
  5. 클라이언트가 인증 코드를 받았다면 인증코드와 code varifier를 인증 서버에보낸다.
  6. 인증 서버는 code verifier를 동일한 알고리즘으로 해싱하여 해시값을 얻은 후 code challenge와 비교한다. 두 값이 일치한다면 액세스 토큰을 발급해준다.
  7. 액세스 토큰을 리소스 서버에 보내서 리소스 서버로부터 자원을 받을 수 있다.

2,3단계에서 인증 서버에 보내야 하는 정보들

  1. client id
  2. redirect uri
  3. scope
  4. state
  5. response type
  6. code challenge
  7. code challenge method

5단계에서 인증 서버에 보내야 하는 정보들

  1. code
  2. client id, client secret(optional) - ui에 client secret을 저장하지 못하는 경우에도 사용가능하다.
  3. grant type
  4. redirect uri
  5. code verifier
  • 초기에는 클라이언트 secret이 모두에게 보여지는 자바 스크립트 같은 ui를 위해 개발된 프로토콜 이지만 무작위 인증 코드 주입과 같은 공격을 방어하기 유용해 많은 기업에서 선호한다고 한다.