OAuth2 RBACの使用
このガイドでは、QuarkusアプリケーションがOAuth2トークンを利用して、Jakarta REST(旧称JAX-RS)エンドポイントへのセキュアなアクセスを提供する方法について説明します。
OAuth2は、アプリケーションがユーザーに代わってHTTPリソースへのアクセスを取得することを可能にする認可フレームワークです。ユーザー認証を外部サーバー(認証サーバー)に委譲し、認証コンテキストにトークンを提供することで、トークンに基づくアプリケーション認証の仕組みを実装することができます。
このエクステンションは、opaque形式のBearerトークンを使用してイントロスペクションエンドポイントを呼び出して検証するための軽量サポートを提供します。
OAuth2認証サーバーがJWTベアラートークンを提供する場合は、代わりに OIDCベアラートークン認証 または SmallRye JWT エクステンションの使用を検討してください。QuarkusアプリケーションでOIDC認可コードフローを使用してユーザーを認証する必要がある場合は、OpenID Connectエクステンションを使用する必要があります。詳細については、 OIDCコードフローメカニズムによるWebアプリケーションの保護 ガイドを参照してください。
ソリューション
次の章で紹介する手順に沿って、ステップを踏んでアプリを作成することをお勧めします。ただし、完成した例にそのまま進んでも構いません。
Git リポジトリのクローン: git clone https://github.com/quarkusio/quarkus-quickstarts.git 、またはアーカイブをダウンロードしてください。
ソリューションは security-oauth2-quickstart
ディレクトリ にあります。ここで作成された Jakarta REST リソースを使用するための非常にシンプルな UI も含まれています。
Mavenプロジェクトの作成
まず、新しいプロジェクトが必要です。以下のコマンドで新規プロジェクトを作成します:
Windowsユーザーの場合:
-
cmdを使用する場合、(バックスラッシュ
\
を使用せず、すべてを同じ行に書かないでください)。 -
Powershellを使用する場合は、
-D
パラメータを二重引用符で囲んでください。例:"-DprojectArtifactId=security-oauth2-quickstart"
このコマンドはプロジェクトを生成し、OAuth2のopaqueトークンのサポートを含む elytron-security-oauth2
エクステンションをインポートします。
Mavenプラグインを使用したくない場合は、ビルドファイルに依存関係を含めるだけでよいです:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-elytron-security-oauth2</artifactId>
</dependency>
implementation("io.quarkus:quarkus-elytron-security-oauth2")
Jakarta REST リソースの調査
以下の内容で src/main/java/org/acme/security/oauth2/TokenSecuredResource.java
ファイルを作成します。
package org.acme.security.oauth2;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
@Path("/secured")
public class TokenSecuredResource {
@GET
@Produces(MediaType.TEXT_PLAIN)
public String hello() {
return "hello";
}
}
これは Elytron Security OAuth2 特有の機能を持たない基本的なRESTエンドポイントなので、いくつか追加してみましょう。
JSR250の一般的なセキュリティアノテーションを使用します。これらのアノテーションは、 セキュリティの使用 ガイドで説明されています。
package org.acme.security.oauth2;
import java.security.Principal;
import jakarta.annotation.security.PermitAll;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.SecurityContext;
@Path("/secured")
@ApplicationScoped
public class TokenSecuredResource {
@GET()
@Path("permit-all")
@PermitAll (1)
@Produces(MediaType.TEXT_PLAIN)
public String hello(@Context SecurityContext ctx) { (2)
Principal caller = ctx.getUserPrincipal(); (3)
String name = caller == null ? "anonymous" : caller.getName();
String helloReply = String.format("hello + %s, isSecure: %s, authScheme: %s", name, ctx.isSecure(), ctx.getAuthenticationScheme());
return helloReply; (4)
}
}
1 | @PermitAll は、認証されているかどうかに関わらず、どのような呼出元からでもアクセス可能であることを示しています。 |
2 | ここでは、呼び出しのセキュリティ状態を検査するために、jakarta REST SecurityContext を注入しています。 |
3 | ここでは、現在のリクエストの ユーザー/呼出元である Principal を取得します。セキュリティー保護されていない呼出の場合、これはnullになりますので、 caller をnullかチェックしてユーザー名を作成します。 |
4 | 私たちが作成した応答では 呼出元の名前、リクエストの SecurityContext の isSecure() と getAuthenticationScheme() の状態を利用しています。 |
application.propertiesの設定
アプリケーションには、以下の最小限のプロパティーを設定する必要があります:
quarkus.oauth2.client-id=client_id
quarkus.oauth2.client-secret=secret
quarkus.oauth2.introspection-url=http://oauth-server/introspect
認証サーバーの イントロスペクションURL と、アプリケーションが認証サーバーへの認証に使用する client-id
/ client-secret
を指定する必要があります。+ エクステンションは、この情報を使ってトークンを検証し、トークンに関連する情報を復元します。
すべての設定プ ロパテ ィ については、 こ のガ イ ド の最後にあ る 設定リファレンス セ ク シ ョ ン を参照 し て く だ さ い。
アプリケーションの実行
これで、アプリケーションを実行する準備が整いました。次を実行してください:
quarkus dev
./mvnw quarkus:dev
./gradlew --console=plain quarkusDev
REST エンドポイントが実行されているので、curl のようなコマンドラインツールを使ってアクセスできます:
$ curl http://127.0.0.1:8080/secured/permit-all; echo
hello + anonymous, isSecure: false, authScheme: null
リクエストでトークンを提供していないため、エンドポイントが見ているセキュリティー状態があるとは期待できず、レスポンスもそれと一致しています:
-
ユーザー名は匿名です
-
isSecure
は https が使用されていないため false です -
authScheme
はnullです
エンドポイントのセキュア化
では実際に何かをセキュア化してみましょう。下記の新しいエンドポイントメソッド helloRolesAllowed
を見てみましょう:
package org.acme.security.oauth2;
import java.security.Principal;
import jakarta.annotation.security.PermitAll;
import jakarta.annotation.security.RolesAllowed;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.SecurityContext;
@Path("/secured")
@ApplicationScoped
public class TokenSecuredResource {
@GET()
@Path("permit-all")
@PermitAll
@Produces(MediaType.TEXT_PLAIN)
public String hello(@Context SecurityContext ctx) {
Principal caller = ctx.getUserPrincipal();
String name = caller == null ? "anonymous" : caller.getName();
String helloReply = String.format("hello + %s, isSecure: %s, authScheme: %s", name, ctx.isSecure(), ctx.getAuthenticationScheme());
return helloReply;
}
@GET()
@Path("roles-allowed") (1)
@RolesAllowed({"Echoer", "Subscriber"}) (2)
@Produces(MediaType.TEXT_PLAIN)
public String helloRolesAllowed(@Context SecurityContext ctx) {
Principal caller = ctx.getUserPrincipal();
String name = caller == null ? "anonymous" : caller.getName();
String helloReply = String.format("hello + %s, isSecure: %s, authScheme: %s", name, ctx.isSecure(), ctx.getAuthenticationScheme());
return helloReply;
}
}
1 | この新しいエンドポイントは /secured/roles-allowed に配置されます |
2 | @RolesAllowed は指定されたエンドポイントに、呼び出し側が “Echoer” または “Subscriber” のいずれかのロールを割り当てている場合にアクセス可能であることを示します。 |
TokenSecuredResource
に追加後、 curl -v http://127.0.0.1:8080/secured/roles-allowed; echo
を実行し、新しいエンドポイントへのアクセスを試してください。出力は次のようになるはずです:
$ curl -v http://127.0.0.1:8080/secured/roles-allowed; echo
* Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to 127.0.0.1 (127.0.0.1) port 8080 (#0)
> GET /secured/roles-allowed HTTP/1.1
> Host: 127.0.0.1:8080
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 401 Unauthorized
< Connection: keep-alive
< Content-Type: text/html;charset=UTF-8
< Content-Length: 14
< Date: Sun, 03 Mar 2019 16:32:34 GMT
<
* Connection #0 to host 127.0.0.1 left intact
Not authorized
素晴らしいことに、リクエストでOAuth2トークンを提供していないので、エンドポイントにアクセスできないはずですが、そうではありませんでした。代わりに、HTTP 401 Unauthorized エラーが発生しました。エンドポイントにアクセスするためには、有効な OAuth2 トークンを取得して渡す必要があります。1) Elytron Security OAuth2 エクステンションにトークンの検証方法を設定する、2) 適切なクレームを含む一致するトークンを生成する。
トークンの生成
標準的なOAuth2認証サーバー(例えば Keycloak )から、トークンのエンドポイントを使用してトークンを取得する必要があります。
以下に、 client_credential
フローに対するこのような呼び出しのcurlの例を示します:
curl -X POST "http://oauth-server/token?grant_type=client_credentials" \
-H "Accept: application/json" -H "Authorization: Basic Y2xpZW50X2lkOmNsaWVudF9zZWNyZXQ="
これは以下のような応答をする筈です.
{"access_token":"60acf56d-9daf-49ba-b3be-7a423d9c7288","token_type":"bearer","expires_in":1799,"scope":"READER"}
最後に、/secured/roles-allowed へのセキュアなリクエストを行います
これを使って /secured/roles-allowed
エンドポイントにセキュアなリクエストをしてみましょう
$ curl -H "Authorization: Bearer 60acf56d-9daf-49ba-b3be-7a423d9c7288" http://127.0.0.1:8080/secured/roles-allowed; echo
hello + client_id isSecure: false, authScheme: OAuth2
成功です! これで以下が得られます:
-
client_id の匿名でない呼出元名
-
OAuth2 の認証スキーム
ロールマッピング
ロールは、イントロスペクションのエンドポイントレスポンスのクレームの1つからマッピングされます。デフォルトでは、 scope
クレームです。ロールはクレームをスペース区切りで分割して取得します。クレームが配列の場合、分割は行われず、ロールは配列から取得されます。
ロールに使用するクレームの名前は、 quarkus.oauth2.role-claim
プロパティーでカスタマイズできます。
アプリケーションをパッケージ化して実行する
いつものように、アプリケーションは以下の方法でパッケージ化できます:
quarkus build
./mvnw install
./gradlew build
そして、 java -jar target/quarkus-app/quarkus-run.jar
を使って実行します:
[INFO] Scanning for projects...
...
$ java -jar target/quarkus-app/quarkus-run.jar
2019-03-28 14:27:48,839 INFO [io.quarkus] (main) Quarkus 3.17.2 started in 0.796s. Listening on: http://[::]:8080
2019-03-28 14:27:48,841 INFO [io.quarkus] (main) Installed features: [cdi, rest, rest-jackson, security, security-oauth2]
ネイティブ実行可能ファイルを ./mvnw clean package -Pnative
で生成することもできます:
quarkus build --native
./mvnw install -Dnative
./gradlew build -Dquarkus.native.enabled=true
[INFO] Scanning for projects...
...
[security-oauth2-quickstart-runner:25602] universe: 493.17 ms
[security-oauth2-quickstart-runner:25602] (parse): 660.41 ms
[security-oauth2-quickstart-runner:25602] (inline): 1,431.10 ms
[security-oauth2-quickstart-runner:25602] (compile): 7,301.78 ms
[security-oauth2-quickstart-runner:25602] compile: 10,542.16 ms
[security-oauth2-quickstart-runner:25602] image: 2,797.62 ms
[security-oauth2-quickstart-runner:25602] write: 988.24 ms
[security-oauth2-quickstart-runner:25602] [total]: 43,778.16 ms
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 51.500 s
[INFO] Finished at: 2019-06-28T14:30:56-07:00
[INFO] ------------------------------------------------------------------------
$ ./target/security-oauth2-quickstart-runner
2019-03-28 14:31:37,315 INFO [io.quarkus] (main) Quarkus 0.20.0 started in 0.006s. Listening on: http://[::]:8080
2019-03-28 14:31:37,316 INFO [io.quarkus] (main) Installed features: [cdi, rest, rest-jackson, security, security-oauth2]
統合テスト
統合テストに本物の OAuth2 認可サーバーを使いたくない場合は、テストに プロパティベースのセキュリティ エクステンションを使うか、Wiremock を使って認可サーバーをモックすることができます。
まず、Wiremockをテストの依存関係として追加する必要があります。Mavenプロジェクトの場合は以下のようになります:
<dependency>
<groupId>org.wiremock</groupId>
<artifactId>wiremock</artifactId>
<scope>test</scope>
<version>${wiremock.version}</version> (1)
</dependency>
1 | 適切なWiremockバージョンを使用してください。利用可能なすべてのバージョンは、 こちら をご覧ください。 |
testImplementation("org.wiremock:wiremock:${wiremock.version}") (1)
1 | 適切なWiremockバージョンを使用してください。利用可能なすべてのバージョンは、 こちら をご覧ください。 |
Quarkusのテストを実行する前にサービスを開始する必要がある場合、Quarkusのテストでは、 @io.quarkus.test.common.QuarkusTestResource
アノテーションを利用して、サービスを開始できる io.quarkus.test.common.QuarkusTestResourceLifecycleManager
を指定し、Quarkusが使用する設定値を提供します。
|
このように QuarkusTestResourceLifecycleManager
の実装である MockAuthorizationServerTestResource
を作成してみましょう:
import com.github.tomakehurst.wiremock.WireMockServer;
import com.github.tomakehurst.wiremock.client.WireMock;
import io.quarkus.test.common.QuarkusTestResourceLifecycleManager;
import java.util.Collections;
import java.util.Map;
public class MockAuthorizationServerTestResource implements QuarkusTestResourceLifecycleManager { (1)
private WireMockServer wireMockServer;
@Override
public Map<String, String> start() {
wireMockServer = new WireMockServer();
wireMockServer.start(); (2)
// define the mock for the introspect endpoint
WireMock.stubFor(WireMock.post("/introspect").willReturn(WireMock.aResponse() (3)
.withBody(
"{\"active\":true,\"scope\":\"Echoer\",\"username\":null,\"iat\":1562315654,\"exp\":1562317454,\"expires_in\":1458,\"client_id\":\"my_client_id\"}")));
return Collections.singletonMap("quarkus.oauth2.introspection-url", wireMockServer.baseUrl() + "/introspect"); (4)
}
@Override
public void stop() {
if (null != wireMockServer) {
wireMockServer.stop(); (5)
}
}
}
1 | start メソッドは、テストを実行する前にQuarkusによって呼び出され、テスト実行中に適用される設定プロパティーの Map を返します。 |
2 | Wiremockを起動します。 |
3 | OAuth2のイントロスペクトレスポンスを返すことで、 /introspect への呼び出しをスタブ化するように Wiremock を設定します。この行をカスタマイズして、アプリケーションに必要なものを返すようにする必要があります (ロールはスコープから派生しているので、少なくともスコープのプロパティーは必要です)。 |
4 | start メソッドはテストに適用される設定を返すので、OAuth2 エクステンションで使用する introspect エンドポイントの URL を制御する quarkus.oauth2.introspection-url プロパティーを設定します。 |
5 | すべてのテストが終了したら、Wiremockをシャットダウンします。 |
この QuarkusTestResourceLifecycleManager
を使用するためには、テストクラスに @QuarkusTestResource(MockAuthorizationServerTestResource.class)
のようなアノテーションを付ける必要があります。
以下は、 MockAuthorizationServerTestResource
を使用したテストの例です。
@QuarkusTest
@QuarkusTestResource(MockAuthorizationServerTestResource.class) (1)
class TokenSecuredResourceTest {
// use whatever token you want as the mock OAuth server will accept all tokens
private static final String BEARER_TOKEN = "337aab0f-b547-489b-9dbd-a54dc7bdf20d"; (2)
@Test
void testPermitAll() {
RestAssured.given()
.when()
.header("Authorization", "Bearer " + BEARER_TOKEN) (3)
.get("/secured/permit-all")
.then()
.statusCode(200)
.body(containsString("hello"));
}
@Test
void testRolesAllowed() {
RestAssured.given()
.when()
.header("Authorization", "Bearer " + BEARER_TOKEN)
.get("/secured/roles-allowed")
.then()
.statusCode(200)
.body(containsString("hello"));
}
}
1 | 以前に作成した MockAuthorizationServerTestResource をQuarkusのテストリソースとして使用します。 |
2 | 任意のトークンを定義してください。OAuth2のモック認可サーバーでは検証されません。 |
3 | このトークンを Authorization ヘッダ内で使用して、OAuth2 認証を開始します。 |
|
設定リファレンス
ビルド時に固定される構成プロパティ - 他のすべての構成プロパティは実行時にオーバーライド可能
Configuration property |
型 |
デフォルト |
---|---|---|
Determine if the OAuth2 extension is enabled. Enabled by default if you include the Environment variable: Show more |
boolean |
|
The claim that is used in the introspection endpoint response to load the roles. Environment variable: Show more |
string |
|
The OAuth2 client id used to validate the token. Mandatory if the extension is enabled. Environment variable: Show more |
string |
|
The OAuth2 client secret used to validate the token. Mandatory if the extension is enabled. Environment variable: Show more |
string |
|
The OAuth2 introspection endpoint URL used to validate the token and gather the authentication claims. Mandatory if the extension is enabled. Environment variable: Show more |
string |
|
The OAuth2 server certificate file. Warning: this is not supported in native mode where the certificate must be included in the truststore used during the native image generation, see Using SSL With Native Executables. Environment variable: Show more |
string |