OAuth 클라이언트 권한 부여를 위한 JWT(JSON Web Token)

OAuth 클라이언트 권한 부여를 위한 JWT를 사용하면 클라이언트가 OAuth 2.0 액세스 토큰 대신 서명된 JWT 토큰을 OpenID Connect 제공자에 전송할 수 있습니다.

OAuth 클라이언트 권한 부여를 위한 JWT는 openidConnectServer-1.0 기능에 포함되어 있습니다. 이를 사용하면 클라이언트가 OAuth 2.0 액세스 토큰 대신 서명된 JWT 토큰을 OpenID Connect 제공자에 전송할 수 있습니다.

이 기능의 예제 사용법 시나리오는 온라인 은행으로부터의 매월 자동 납부에 권한을 부여하는 전기 회사의 고객입니다. 전기 회사와 온라인 은행이 해당 요청을 이행하기 위해 신뢰할 수 있는 관계를 설정했다고 가정해 봅니다. 전기 회사는 매월 OAuth 2.0 액세스 토큰을 요청하기 위해 온라인 은행에 대해 구성된 OpenID Connect 제공자의 토큰 엔드포인트 URI에 적절한 청구가 포함된 서명된 JWT 토큰을 전송할 수 있습니다. 그런 다음 전기 회사는 해당 액세스 토큰을 사용하여 온라인 은행으로부터 매월 납부를 받을 수 있습니다.

OAuth 2.0 클라이언트 인증 및 권한 부여를 위한 JWT (JSON Web Token) 프로파일 스펙의 일부는 OpenID Connect 제공자로 구성된 자유 서버에 대해 지원됩니다. JWT 클라이언트 기능을 지원하려는 사용자는 자체 애플리케이션을 사용하여 지원해야 합니다.

권한 부여된 범위

OpenID Connect 클라이언트는 JWT가 포함된 HTTPS 요청을 OpenID Connect 제공자의 토큰 엔드포인트에 전송하여 액세스 토큰을 요청합니다. 이 프로세스 동안 사용자에게는 범위 사용 권한을 부여하는 데 필요한 승인 양식이 표시되지 않습니다. JWT 핸들러는 다음과 같은 기준을 따르는 권한 부여된 범위를 처리합니다.

  1. 요청에 범위 매개변수가 지정되지 않은 경우에는 OpenID Connect 제공자가 액세스 토큰에서 범위를 지정하지 않습니다.
  2. OpenID Connect 클라이언트가 OpenID Connect 제공자 구성에서 autoAuthorized 클라이언트로 규정되지 않은 경우에는 요청에서 해당 클라이언트에 의해 지정되는 범위가 모두 액세스 토큰의 범위 목록에서 지정됩니다.
  3. OpenID Connect 클라이언트가 autoAuthorized 클라이언트로 규정되지 않은 경우에는 요청에 포함된 범위를 클라이언트 구성의 범위 목록으로 필터링해야 하며 preAuthorizedScope 목록에서도 정의해야 합니다. HTTPS 요청에 있는 범위가 클라이언트 구성의 scopepreAuthorizeScope 목록에 있는 경우 해당 범위는 액세스 토큰의 범위 목록에서 지정할 수 있습니다.

클라이언트가 autoAuthorized 클라이언트로 규정되지 않은 경우에는 액세스 토큰의 범위 목록에 포함될 수 있는 범위를 클라이언트 구성에서 적절하게 구성해야 합니다. 이 범위는 OpenID Connect 제공자에 대한 클라이언트 구성에 있는 scopepreAuthorizedScope 속성에 대한 값에 포함되어야 합니다. 표시되는 예제에서는 profileemail 범위가 scopepreAuthorizedScope 값 목록에 포함되므로 액세스 토큰의 범위 목록에서 이들 범위가 지정됩니다. 범위가 클라이언트 구성의 scope 속성에 나열되지 않는 경우 해당 범위는 액세스 토큰의 범위 목록에서 생략됩니다. 범위가 scope 속성에 나열되어 있지만 클라이언트 구성 내의 preAuthorizedScope 목록에 포함되어 있지 않으면 권한 부여 요청이invalid_grantOpenID Connect 제공자의 응답에 오류가 있습니다.


<openidConnectProvider id="OidcConfigSample" oauthProviderRef="OAuthConfigSample" />
<oauthProvider id="OAuthConfigSample" ... >
        ...
        <localStore>            
            <client name="client01" secret="{xor}..."
                    displayname="client01"
                    scope="profile email phone"
                    preAuthorizedScope="profile email"
                    enabled="true"/>
            ...
        </localStore>
    </oauthProvider>

JWT(JSON Web Token)에서 청구

올바른 JWT(JSON Web Token)에 서명해야 합니다. OpenID Connect 제공자로 구성된 Liberty 서버는 토큰 서명 알고리즘으로 HMAC-SHA256 만 지원합니다. 각 OpenID Connect 클라이언트의 서명 키는 OpenID Connect 제공자의 클라이언트 구성에 있는 secret 속성입니다. 표시된 예제에서 사용되는 서명 키는 "{xor}LDo8LTor"입니다.

<client name="client01" displayname="client01" secret="{xor}LDo8LTor" ... />

OpenID Connect 제공자는 JWT에서 다음과 같은 청구도 확인합니다.

'iss'(발행자)
이 청구는 JWT에서 필수입니다. iss 청구는 OpenID Connect 제공자에서 클라이언트 구성의 name 속성 또는 redirect 속성과 일치되어야 합니다. 다음 예제에서 iss 청구는 client01 또는 http://op201406.ibm.com:8010/oauthclient/redirect.jsp와 일치해야 합니다.
<client name="client01" redirect="http://op201406.ibm.com:8010/oauthclient/redirect.jsp" scope="openid profile email" ... />
'sub'(주제)
이 청구는 JWT에서 필수입니다. 주제의 값은 OpenID Connect 제공자 서버의 사용자 레지스트리에서 올바른 사용자 이름이어야 합니다.
'aud'(대상)
이 청구는 JWT에서 필수입니다. 대상 청구의 값은 issuerIdentifier 속성이 openidConnectProvider 구성에서 지정된 경우 issuerIdentifier의 이름입니다. issuerIdentifier 속성이 openidConnectProvider 구성에서 지정되지 않은 경우 대상은 OpenID Connect 제공자의 토큰 엔드포인트 URI여야 합니다. 다음 예제에서 대상 청구의 값은 "OpenIDConnectProviderID1"입니다.
<openidConnectProvider id="OidcConfigSample" oauthProviderRef="OAuthConfigSample" issuerIdentifier="OpenIDConnectProviderID1" />
'exp'(만기)
이 청구는 JWT에서 필수이며 JWT를 사용할 수 있는 시간 창을 제한합니다. OpenID Connect 제공자는 해당 시스템 클럭 및 일부 허용 가능한 클럭 오차에 대해 exp를 확인합니다.
'nbf'(이후)
이는 선택적 청구입니다. 존재하는 경우 토큰은 이 청구에 의해 지정된 시간 이후에만 유효합니다. OpenID Connect 제공자는 해당 시스템 클럭 및 일부 허용 가능한 클럭 오차에 대해 이 시간을 확인합니다.
'iat'(발행된 시간)
기본적으로 이는 선택적 청구입니다. 하지만 jwtGrantType 요소의 iatRequired 속성이 true로 설정된 경우에는 모든 JWT가 iat 청구를 포함해야 합니다. 존재하는 경우 iat 청구는 JWT가 발행된 시간을 표시합니다. JWT는 maxTokenLifetime보다 오래 발행할 수 없습니다.
'jti'(JWT ID)
이는 선택적 청구이며 JWT 토큰의 고유 ID입니다. 존재하는 경우 동일한 JWT ID를 발행자가 재사용할 수 없습니다. 예를 들어, client01jtiid6098364921인 JWT를 발행하는 경우 client01 에서 발행한 다른 JWT의 jti 값은 id6098364921가 될 수 없습니다. 다른 JWT와 동일한 jti 청구를 가진 JWT는 반복 공격으로 간주됩니다. OpenID Connect 제공자로 구성된 Liberty 서버는 서버에 jti 캐시를 설정합니다. 이 캐시의 크기는 jwtGrantType 구성의 maxJtiCacheSize에 의해 지정됩니다. 캐시에 보관되는 jti ID는 새 수신 jti ID에 대해 확인됩니다. 캐시가 가득 찬 경우가 아니면 캐시에 저장된 jti ID는 제거되지 않습니다.

JWT(JSON Web Token) 요청 제출

HTTP 대신 HTTPS 프로토콜을 사용하여 JWT 요청을 제출하는 것이 우수 사례입니다. OpenID Connect 제공자의 토큰 엔드포인트는 HTTPS JWT 요청을 처리하는 데 사용됩니다. OpenID Connect 제공자에 대한 토큰 엔드포인트를 판별하려면 OpenID Connect에 대한 토큰 엔드포인트 호출 또는 OAuth 엔드포인트 URL을 참조하십시오.

요청은 다음과 같은 매개변수를 포함해야 합니다.

  • grant_type - 이 매개변수의 값은 "urn:ietf:params:oauth:grant-type:jwt-bearer"여야 합니다.
  • assertion - 이 매개변수의 값은 단일 서명된 JWT 토큰을 포함해야 합니다.
  • scope - 이 매개변수는 선택사항입니다. scope이 생략되는 경우 리턴되는 액세스 토큰은 범위를 포함하지 않습니다. scope 매개변수에서 나열되는 범위 값은 OpenID Connect 제공자 구성에 대해 확인됩니다. 자세한 정보는 이전의 권한 부여된 범위 절을 참조하십시오.
  • client_id - 이 매개변수의 값은 OpenID Connect 제공자의 클라이언트 구성에 있는 name 속성과 일치해야 합니다.
  • client_secret - 이 매개변수의 값은 OpenID Connect 제공자의 클라이언트 구성에 있는 secret 속성과 일치해야 합니다.

예제 HTTPS 요청:

POST /token.oauth2 HTTP/1.1
    Host: oidc.ibm.com
    Content-Type: application/x-www-form-utlencoded

    client_id=client01
    &client_secret=secret     
    &grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer     
    &assertion=eyJhbGc[---omitted---]kIn0.eyJpc[---ommitted---]A4fQ.MB6ZFlCsHg5MJ-weIHZYz6xgF1jdSZn7ErchHs8-8Rk     
    &scope=profile email

서명된 JWT 토큰을 작성하는 Java 예제:

package com.ibm.sample;

import java.security.SignatureException;
import com.google.gson.JsonObject;
import net.oauth.jsontoken.crypto.HmacSHA256Signer;

import net.oauth.jsontoken.SystemClock;
import net.oauth.jsontoken.JsonToken;
import org.joda.time.Duration;
import org.joda.time.Instant;

public class SampleJWTToken {
        private static final Duration SKEW = Duration.standardMinutes(5);

        JsonToken jwtToken = null;
        String[] allPayloadKeys = { "iss", "sub", "aud", "exp", // required
                                    "nbf", "iat", "jti" }; // optional

        public SampleJWTToken(String clientId, 
                              String keyId,
                              String signKey,
                              String audience, 
                              String subject, // user
                              String jtiId) throws Exception { // InvalidKeyException

                byte[] hs256Key = signKey.getBytes();
                HmacSHA256Signer hmacSha256Signer = new HmacSHA256Signer(
                                clientId, keyId, hs256Key);
                // _rsaSha256Signer = new RsaSHA256Signer(clientId, _keyId,
                //                                        _privateKey);
                SystemClock clock = new SystemClock(SKEW);
                jwtToken = new JsonToken(hmacSha256Signer, clock);
                JsonObject headerObj = jwtToken.getHeader();
                JsonObject payloadObj = jwtToken.getPayloadAsJsonObject();

                headerObj.addProperty("alg", "HS256");

                Instant instantExp = clock.now().plus(600000); // 10 minutes
                jwtToken.setExpiration(instantExp);
                jwtToken.setAudience(audience);
                payloadObj.addProperty("iss", clientId);
                payloadObj.addProperty("sub", subject);

                // optional
                payloadObj.addProperty("jti", jtiId);
                jwtToken.setIssuedAt(clock.now()); // issued at time
        }

        public String getJWTTokenString() throws Exception {
                String signedAndSerializedString = null;
                try {
                        signedAndSerializedString = jwtToken.serializeAndSign();
                } catch (SignatureException e) {
                        throw e;
                }
                return signedAndSerializedString;
        }
}