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 핸들러는 다음과 같은 기준을 따르는 권한 부여된 범위를 처리합니다.
- 요청에 범위 매개변수가 지정되지 않은 경우에는 OpenID Connect 제공자가 액세스 토큰에서 범위를 지정하지 않습니다.
- OpenID Connect 클라이언트가 OpenID Connect 제공자 구성에서 autoAuthorized 클라이언트로 규정되지 않은 경우에는 요청에서 해당 클라이언트에 의해 지정되는 범위가 모두 액세스 토큰의 범위 목록에서 지정됩니다.
- OpenID Connect 클라이언트가 autoAuthorized 클라이언트로 규정되지
않은 경우에는 요청에 포함된 범위를 클라이언트 구성의 범위 목록으로
필터링해야 하며
preAuthorizedScope
목록에서도 정의해야 합니다. HTTPS 요청에 있는 범위가 클라이언트 구성의scope
및preAuthorizeScope
목록에 있는 경우 해당 범위는 액세스 토큰의 범위 목록에서 지정할 수 있습니다.
클라이언트가 autoAuthorized 클라이언트로 규정되지
않은 경우에는 액세스 토큰의 범위 목록에 포함될 수 있는 범위를
클라이언트 구성에서 적절하게 구성해야 합니다. 이 범위는 OpenID
Connect 제공자에 대한 클라이언트 구성에 있는 scope
및
preAuthorizedScope
속성에 대한 값에 포함되어야 합니다. 표시되는 예제에서는 profile
및 email
범위가 scope
및 preAuthorizedScope
값 목록에
포함되므로 액세스 토큰의 범위 목록에서 이들 범위가 지정됩니다. 범위가 클라이언트 구성의 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를 발행자가 재사용할 수 없습니다. 예를 들어,
client01
가jti
이id6098364921
인 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;
}
}