Kopring | Security and OpenID Connect

DoItDev·2022년 1월 21일
0
post-thumbnail

Overview

OpenID 필터를 구현하기 위해서는 Oauth2 에서 어떤식으로 로직이 이루어지는지 그리고 어떤식으로 데이터를 받아오는지 그리고 주고 받는 지를 먼저 알아야된다.

Oauth2와 연동하는 Google 또는 Kakao 등 .. 으로 기준으로 설명을 하면 email 과 password를 위의 서비스 하는 기업에서 제공하는 api에 태워준다.

그렇게 태운뒤 거기서 id_token 그리고 access_token을 내려준다.여기서 id_token는 유저의 정보를 담고 있다. 하지만 이 정보 또한 커스텀이 가능하게 설정이 되기 때문에 꼭 똑같이 안해도 된다.

그렇게 id_token의 경우 서버에서 다시 인증을 시켜준다. 이때 사용되는 open_id 필터를 구현을 하려고한다.

id_token 을 인증해주는 필터 구현

여기서 id_token 의 경우 kid 값과 jwt 로 파싱이 가능하다. 그래서 filter uri 를 세팅을 해준뒤 그 api uri 를 타고 들어오면 캐치해서 가지고 와서 필터 로직을 태우는 식으로 진행을 하게 되었다.

또한 기존에 OAuth2 에서 사용하는 형식이 아니라 조금은 커스텀한 필터기 때문에 OAuth2에서 사용하는 방식과는 유사하면서 다른점이 있다.

일단 UI 에서 OAuth2 에 대한 로직이 있고 서버에서는 ID_TOKEN 혹은 ACCESS_TOKEN 만 가지고 진행을 하기 때문에 굳이 서버에서 OAuth 로직을 만들 필요가 없다고 판단을 하게 되었다.

class OpenIdConnectFilter : AbstractAuthenticationProcessingFilter {

    constructor(
        defaultFilterProcessesUrl: String,
    ) : super(
        defaultFilterProcessesUrl
    ) {
        // NoAuth manager class
        authenticationManager = NoOpAuthenticationManager()
    }

    @Throws(Exception::class)
    override fun attemptAuthentication(request: HttpServletRequest?, response: HttpServletResponse?): Authentication? {

        val method = request!!.method

        if (HttpMethod.POST.name != method) {
            throw NotPostMethodException()
        }

        val requestBody = getByRequestBodyToMap(request)

        val idToken = requestBody["id_token"]

        if (idToken!!.isEmpty()) {
            throw IdTokenEmptyException()
        }

        val kid = JwtHelper.headers(idToken)["kid"] as String

        val tokenDecoded: org.springframework.security.jwt.Jwt? = JwtHelper.decodeAndVerify(idToken, verifier(kid))

        val authInfo: MutableMap<*, *>? = ObjectMapper().readValue(tokenDecoded?.claims, MutableMap::class.java)

        val userName = authInfo?.get("email") as String
        
        // user 정보를 담는 로직  

        val domainUserDetail = DomainUserDetail(userOptional.get())

        return UsernamePasswordAuthenticationToken(domainUserDetail, null, domainUserDetail.authorities)
    }

    /**
     * Get by request body to map </br>
     * @description 요청에서 From 으로 넘길 경우 데이터를 읽고 가지고는 메소드
     * @param request
     * @return
     */
    private fun getByRequestBodyToMap(request: HttpServletRequest?): Map<String, String> {

        val objectMapper = ObjectMapper()

        val requestBody = request!!.reader.lines().collect(Collectors.joining(System.lineSeparator()))

        return objectMapper.readValue(requestBody, Map::class.java) as Map<String, String>
    }

    /**
     * kid 체크로 google check 값 리턴
     * @param kid
     * @return
     */
    @Throws(Exception::class)
    private fun verifier(kid: String): RsaVerifier? {
        val provider: JwkProvider = UrlJwkProvider(URL(jwkUrl))
        val jwk = provider[kid]
        return RsaVerifier(jwk.publicKey as RSAPublicKey)
    }

    /**
     * Verify claims
     *
     * @param claims
     * @param clientId
     * @param issuer
     */
    private fun verifyClaims(claims: Map<*, *>, clientId: String, issuer: String) {
        val exp = claims["exp"] as Int
        val expireDate = Date(exp * 1000L)
        val now = Date()
        if (expireDate.before(now) || claims["iss"] != issuer || claims["aud"] != clientId) {
            throw RuntimeException("Invalid claims")
        }
    }

}

AbstractAuthenticationProcessingFilter 의 경우 세팅된 URI로 접근이 할 경우 가로채서 필터를 타게 된다.

로그인 할때 처음에 시작되는 필터로 Authority Filter 중 UsernamePasswordAuthenticationFilter.class 앞에서 필터를 처리하게 해주면된다.

AbstractAuthenticationProcessingFilter 의 경우 catch 할수 있는 url 과 AuthenticationManager 에 대한 인증에 대한 세팅이 가능하다 그리고 success & fail handler 에 대한 세팅을 해 줄 수도 있다.

UsernamePasswordAuthenticationToken 을 넘겨 주게 되어있는데 여기서 객체를 인스턴스화 할때 UserDetails 에서 User 를 상속을 받아서 사용을 하게 되었다.

usernamePasswordAuthenticationToken 을 인스턴스 할때 principal,credentials 의 경우 생성자에 object 로 명시가 되어있기 때문에 프로젝트에 맞게 사용을 하면된다. (cast 해서 사용이 가능함으로 객체 설계가 가능하다.)

google 의 경우 kid 값으로 세팅을 하게된다. kid 값이란 암호화 되는 키를 의미를하는데 위의 코드에서 verifier method 를 보게 되면 goolge 에서 제공해주는 web uri 가 있다.

여기서 나의 경우 application.yml 에서 사용을 하는데 @Value어노테이션을 사용을 해서 filter 에서 데이터을 가지고 와서 사용을 하게 시켜 주었다.

verifyClaims method의 경우 id_token 에 대한 시간 체크를 해주는 메소드이다. 그렇게 때문에 잘못된 id_token 에 대한 시간 값이 들어온다면 체크를 해준다.

이렇게 필터를 만든 뒤 NoOpAuthenticationManagerauthentication 인증 객체를 던져준다.

그렇다면 NoOpAuthenticationManager 가 무엇인가 보통은 Provider 를 일을시킨다. Provider에 대한 로직을 만들지만 이번 구글 인증의 경우에는 사용하지 않기 때문에 아래와 같이 바로 넘겨주면 된다.

class NoOpAuthenticationManager : AuthenticationManager {
    override fun authenticate(authentication: Authentication?): Authentication? {
        return authentication
    }
}

이렇게 인증이 되면 그후에 successHandler & failureHandler 가 성공 혹은 에러를 캐치해서 로그인 프로세스가 진행이 된다.

profile
Back-End Engineer

0개의 댓글