Upgrade to Pro — share decks privately, control downloads, hide ads and more …

MicroProfile JWTを使ってマイクロサービスをセキュアにしよう

MicroProfile JWTを使ってマイクロサービスをセキュアにしよう

2022年11月27日開催された日本Javaユーザーグループのカンファレンス「JJUG CCC 2022 Fall」で実施したセッションです。

内容
- JWTトークンを使う認証認可がはじまった経緯
- JWTトークンの内容
- MicroProfile JWTの概要
- JWTトークン送受信のコーディング例
- Quarkusの上で動くサービスから、OpenLibertyの上で動くサービスの間でJWTを使った認証認可のデモ

Hiroko Takamiya

November 28, 2022
Tweet

More Decks by Hiroko Takamiya

Other Decks in Programming

Transcript

  1. ⾼宮裕⼦(たかみや ひろこ) • ⽶IBM所属 • ノースカロライナ州在住 • アプリケーションサーバー開発部⾨ • セキュリティチームに約10+年

    • 東京⽣まれ、東京育ち • JJUG初参加です • よろしくお願いします︕ ⾃⼰紹介 画像︓Wikipedia/ノースカロライナ州観光協会
  2. トーク概要 MicroProfile JWTの概要を紹介 • サービスから別のサービスへと安全にアクセスする仕組み § マイクロサービスのセキュリティ関連⽤語 § なぜJWTトークンを使うか §

    JWTトークンについて § JWTトークンの内容、信⽤できるのはなぜ︖ § JWT、JWS、JWE、JWKの違いは︖ § MicroProfile JWT (mpJWT)は、マイクロサービス間の運⽤性を⾼める § 代表的なユースケース § デモ JWTトークン クライアント サービスA サービスB サービスC サービスC
  3. マイクロサービスのセキュリティ関連⽤語 OAuth2 • 認可(Authorization)のプロトコル • OAuth2はソーシャルネットワー クのログインなどに使われる OpenID Connect •

    OAuth2をもとに構築されたオー プン・スタンダード • OpenID Connectをサポートする プロバイダがユーザー認証 (Authenticate)する • クライアントサービス(Relying Party)は、プロバイダを使って認 証するため、⾃前でユーザーデー タを持たないでよい JWT • JSON Web Token • 情報(Claim)を運ぶトークン • ClaimをJSONオブジェクトで表現、 エンコードののち、デジタル署名 または暗号化、もしくはその両⽅ を使って安全に送受信する
  4. なぜマイクロサービスの認証、認可にJWTを使うのか • マイクロサービスはステートレスである • セキュリティ コンテキストを、サーバー側の HTTP セッションに保存しない • マイクロサービス

    クライアントに関連付けられたセキュリティ コンテキストは、通 常、JWT としてリクエスト毎に使われる • JWTが次のマイクロサービスに伝播、送信される 7
  5. 9

  6. JSON Web Token (JWT) 定義: (RFC 7519, https://tools.ietf.org/html/rfc7519より) • 情報(Claim)を伝達する⽅法

    • JWT内のClaimはJSONオブジェクトとしてエンコードされる • エンコードは、URL Safeな形式(“/”や”?”を使わずURLの⼀部として送れる) • エンコードされたデータは • JSON Web Signature (JWS) 構造のペイロードとして署名または、 • JSON Web Encryption (JWE)構造の⽂字データとして署名・暗号化される 10
  7. JWT, JWS, JWE, JWKの違いは何︖ 12 • 署名付き(Signed) JWTは、JWS (JSON Web

    Signature). • 暗号化された(Encrypted)JWTは、JWE (JSON Web Encryption). • 実際、オブジェクトは JWS または JWE のいずれか • JWT は抽象クラスのようなもので、JWS と JWE がセキュアな実装 • JSON Web Key (JWK) は、暗号鍵を表す JSON オブジェクトです • この鍵は、JWE を復号化、または JWS が署名を検証するために使⽤されます (JWT 発⾏者の公 開鍵が含まれています) • JWKキーセット(JSON形式で表されたキーのグループ JWKのArray)というものもある • 詳しくは JSON Object Signing and Encryption spec (JOSE) を参照 • https://tools.ietf.org/html/rfc7520
  8. 署名済みJWT (JWS) 署名済み JWT は JWS と呼ばれます。ドット (.) で区切られた 3

    つの base64 でエンコードされた JSON オブジェク ト (ヘッダー、ペイロード、署名) で構成されます。 Sample decoded JWT { "typ": "JWT", "alg": "RS256" }. { "aud": "server", "iss": "https://ibm.com/oidc/endpoint/OP", "iat": 1311281970, "exp": 1311283970, "sub": “tom tom”, “email": “[email protected]”, }. {JWTの署名} Base64 URL encoded ヘッダー Base64 URL encoded ペイロード Base64 URL encoded 署名 . . 13 このRS256は、発⾏者(Issuer)の秘密鍵 によって署名が作成されていることを 表します。受け取り側は発⾏者の公開 鍵によって、署名を検証します。
  9. 暗号化済みJWT (JWE) 14 • 暗号化された JWS • ドット (.) で区切られた

    5 つの Base64 URL エンコードされた JSONオブジェクト • ヘッダー、暗号化鍵、初期化ベクトル、暗号⽂、および認証タグ ヘッダー JWE Header 暗号化鍵 Encrypt Key 初期化ベクトル Initialization vector 暗号化された内容 Encrypted JWS 認証タグ Authentication tag . . . . 複雑な仕組みです︕ 1) 発⾏者が JWS とランダムな⼀時キーを作成 2) 発⾏者は、受信者の公開鍵を使⽤して⼀時的な鍵を暗号化し、「暗号化鍵」と して JWE に保存します (* 受信者の公開鍵へのアクセスが必要です*) 3) 受信者は独⾃の秘密鍵を使⽤して暗号化鍵を復号化します 4) 受信者は暗号⽂を復号化して JWS を取得します
  10. JWTトークンを⼊⼿するには • JWT は、サーバー間通信で、信頼のおけるサーバーが発⾏できる。こ の場合、呼ばれた側のサーバーは、呼び出し側サーバーによって保証 されたトークンを信頼する • セキュリティ リバース プロキシ

    サーバーもJWTを発⾏できる。この リバースプロキシサーバーは、ログイン後の JWT 作成をサポートする • JWT は、信頼できる OpenID Connect プロバイダー (OP) によっても 発⾏できる (例: Identity Managers, Ping, Azure, Keycloak) 16
  11. 17

  12. MicroProfile JWT (MP-JWT) マイクロサービスにおいて、セキュリティ トークンとして JWT を使⽤する際の相互運⽤性を促進する仕様 仕様の詳細は下記のリリースノートを参照ください https://microprofile.io/project/eclipse/microprofile-jwt-auth/spec/src/main/asciidoc/release-notes.asciidoc 18

    バージョン 内容 MP-JWT 1.0 相互運⽤可能な JWT トークン形式を定義 トークン アクセス API を定義 MP-JWT 1.1 MicroProfile Config を使⽤するポータブル JWT 構成を定義 JSON Web Key (JWK) のサポート MP-JWT 1.2 JWT を Cookie に含めることができます 署名のアルゴリズム追加 MP-JWT 2.0 JakartaEE対応
  13. MicroProfile JWTに必要な情報 19 相互運⽤のために、必要な情報を定義. 太字は必須、太字でないものは推奨 {//ヘッダー “typ”: “JWT”, //トークンのタイプ “alg”:

    “RS256”, // 署名のアルゴリズム } {{//Claim “iss”: “https://server.example.com”, //発⾏者(Issuer) “aud”: “s6BhdRkqt3”, //トークンが対象とする受信者(audience) “jti”: “a-123”, //トークンのID(Identifier) “exp”: 1311281970, //トークンの期限(Expiration) “iat”: 1311280970, //トークン発⾏時(IssuedAt) “sub”: “24400320”, //ユーザーのSubject (java.security.Subject) "upn": “[email protected]", //ユーザーPrincipal (java.security.Principal) “groups”: [“red-group”, “green-group”], //ユーザーが所属するグループ(認可に使われる) “custom-value”: “Javaユーザーグループ” //アプリに必要な情報があればClaimにして送ることができる } { //署名 } (https://github.com/eclipse/microprofile-jwt-auth/pull/191 for MP-JWT1.2より)
  14. 関連するMicroProfile Config 21 下記の構成は、MicroProfile Configの中のMP-JWT関連のもの https://github.com/eclipse/microprofile-jwt-auth/blob/master/spec/src/main/asciidoc/configuration.asciidoc mp.jwt.token.header mp.jwt.token.cookie mp.jwt.verify.audiences mp.jwt.decrypt.key.location

    mp.jwt.verify.publickey.algorithm mp.jwt.verify.publickey mp.jwt.verify.publickey.location mp.jwt.verify.issuer … 構成を別にまとめて、コードから参照することで、コードを変更せず、構成を変えるだけで、 サービスがよりポータブルになります
  15. トークンの作り⽅は︖(start.microprofile.io) import io.vertx.ext.auth.JWTOptions; import io.vertx.ext.auth.PubSecKeyOptions; import io.vertx.ext.auth.jwt.JWTAuth; import io.vertx.ext.auth.jwt.JWTAuthOptions; …

    private static String generateJWT(String key) { JWTAuth provider = JWTAuth.create(null, new JWTAuthOptions() .addPubSecKey(new PubSecKeyOptions() .setAlgorithm("RS256") .setSecretKey(key) )); MPJWTToken token = new MPJWTToken(); token.setAud("targetService"); token.setIss("https://server.example.com"); // Must match configuration values token.setUpn("Jessie"); token.addAdditionalClaims("custom-value", "Jessie specific value"); token.setGroups(Arrays.asList("user", "protected")); return provider.generateToken(new io.vertx.core.json.JsonObject().mergeIn(token.toJSONString()), new JWTOptions().setAlgorithm("RS256")); } 22
  16. トークンの作り⽅は︖(Quarkus) import io.smallrye.jwt.build.JwtClaimsBuilder; public static String generateToken(String username, Set<Role> roles,

    Long duration, String issuer) throws Exception { String privateKeyLocation = "/privatekey.pem"; PrivateKey privateKey = readPrivateKey(privateKeyLocation); JwtClaimsBuilder claimsBuilder = Jwt.claims(); long currentTimeInSecs = currentTimeInSecs(); Set<String> groups = new HashSet<>(); for (Role role : roles) groups.add(role.toString()); claimsBuilder.issuer(issuer); claimsBuilder.subject(username); claimsBuilder.issuedAt(currentTimeInSecs); claimsBuilder.expiresAt(currentTimeInSecs + duration); claimsBuilder.groups(groups); return claimsBuilder.jws().signatureKeyId(privateKeyLocation).sign(privateKey); } 23
  17. トークンの作り⽅は︖(OpenLiberty) import com.ibm.websphere.security.jwt.JwtBuilder; import com.ibm.websphere.security.jwt.Claims; private String buildJwt(String userName, Set<String>

    roles) throws Exception { return JwtBuilder.create("jwtFrontEndBuilder") .claim(Claims.SUBJECT, userName) .claim("upn", userName) .claim("groups", roles.toArray(new String[roles.size()])) .claim("aud", "systemService") .buildJwt() .compact(); https://openliberty.io/docs/latest/reference/config/jwtBuilder.html 24
  18. トークンの送り⽅(1) サーバーがAuthorizationヘッダーにJWTトークンを⼊れて送っているコード WebTarget target = ClientBuilder.newClient().target(serviceB); Response response = target.request().header("authorization",

    "Bearer " + jwt).buildGet().invoke(); 25 JWTトークンは、Authorizationヘッダーの中に、Bearerトークンとして送る HTTP Requestの例 GET /endp/echo HTTP/1.1 Host: server.example.com Authorization: Bearer <JWE> レスポンス例(認証されて、相⼿サービスからGreetingが来たとき) HTTP/1.1 200 OK Hello, [email protected]
  19. トークンの送り⽅(2) JWTトークンをクッキーにするコード(レスポンスとして送付) String jwt = tokenProvider.generateToken(...); Cookie cookie = new

    Cookie(“Bearer”, jwt); //mp.jwt.token.cookieで構成できる cookie.setHttpOnly(true); cookie.setMaxAge(ExpirationTime); cookie.setSecure(true); response.addCookie(cookie); 26 サーバーが、JWTトークンを作って、Cookieにして返しているコード HTTP Requestの例 GET /endp/echo HTTP/1.1 Host: server.example.com Cookie: Bearer=<JWS> JWTトークンは、リクエストの中のCookieとして送付することもできる
  20. MicroProfile JWT – 認証の仕⽅ MP-JWT は認証トークンであり、ロールに直接マップできるグループ属性が含まれています。 セキュリティ ロール名とグループ名が同じ場合、 @RolesAllowed アノテーションが使える

    @Inject @Claim("custom-value") private ClaimValue<String> custom; @GET @RolesAllowed(“protected") public String getJWTBasedValue() { if (custom != null) { return "Protected Resource; Custom value : " + custom.getValue(); } } またトークンのオブジェクトを、APIで読み込んで、Claimを検証することもある 27
  21. アプリからのアクセス アプリケーションは、SecurityContext アノテーションから JsonWebToken にアクセスできる • 次の例では、UserPrincipalを org.eclipse.microprofile.jwt.JsonWebToken API のインスタンスとしてキャストして、

    アプリケーションは JsonWebToken ゲッターを介してすべてのクレームにアクセスできます @GET @Path("/getGroups") public Set<String> getGroups(@Context SecurityContext sec) { Set<= null; Principal user = sec.getUserPrincipal(); if (user instanceof JsonWebToken) { JsonWebToken jwt = (JsonWebToken) user; groups= = jwt.getGroups(); } return groups; } アプリケーションは、Raw Type、ClaimValue、javax.inject.Provider、および JSON-P タイプを介して、 org.eclipse.microprofile.jwt.JsonWebToken API を直接注⼊することもできます @Inject private JsonWebToken jwt; @Inject @Claim(standard= Claims.raw_token) private String rawToken; @Inject @Claim("iat") private Long dupIssuedAt; @Inject @Claim("sub") private ClaimValue<Optional<String>> optSubject; 28
  22. 29

  23. Web リソースと サービス トークンでサー ビスをリクエス トする(JWT) JWT クレームに基づい て認証決定を⾏う アプリケーション・サーバー

    クライアント アプリケー ション • クライアントアプリがJWTを作成し、JWTでサービスを呼び出す • HTTP client: CLI, Restful Service Client ユースケース: シンプルな HTTPS クライアント 30
  24. フロントエンド Web アプリケー ション、またはマ イクロサービス トークンでサー ビスをリクエス トする(JWT) マイクロサービス JWT

    JWT クレームに基づ いて認証決定を⾏う アプリケーション・サー バー 1. ユーザー要求は常に認証リバース プロキシを通過する (エンタープライズ アプリケーションでは⼀般的な構成) 2. サービスは JWT を検証し、サブジェクトを作成する 3. サービスはサブジェクトで、リクエストを承認する 4. サービスは、JWT (オリジナルまたは⾃⼰発⾏) を他のサービスに伝搬する リバース プロキシ サーバー ユーザーの認証 JWT の提供 • ユーザー認証はリバース プロキシ サーバーによって実⾏され、既存の認証サービスと連携する場合があります • リバース プロキシ サーバーは、ユーザーに代わって JWT をセキュリティ トークンとして提供します リバース プロキ シ サーバー経由 で Web を閲覧 する ユースケース: リバース プロキシ セキュリティ サーバー ブラウザ/ クライアント 31 JWT クレームに基づ いて認証決定を⾏う
  25. JWT を使⽤したシングル サインオン⽤の Open ID Connect ユースケース: Open ID Connect

    プロバイダー/クライアント ブラウザ/ クライアント アプリケーションが サーバー付属の OIDC クライアント機能 (Replying Party) を使⽤ OIDCプロバイダ(OP) マイクロサービス1 JsonWebToken は、CDI または JAX-RS SecurityContext を介してアクセスできます。追加の認証に JWT を使⽤する か、JWT を別のサービスに伝播します マイクロサービス2 propagate JWT 認可サーバー︓アクセストークン(JWT)とIDトークン (JWT)を発⾏ 1 2 3 4 5 6 propagate JWT リソース サーバー: バックエンド リポジトリへの認証を実 ⾏します • LDAP based with username or client cert. • Use of SAML external identity provider • Use of external OIDC provider • Use of social medium (ie: Facebook, google) • Use of TAI (trust association interceptor) 7 1. ユーザーは アプリケーションサーバーの OIDC認証機能を使⽤して、使⽤するアプリ にログイン 2. アプリケーションサーバーは、OIDCクライ アント(RP)として振舞い、ユーザーを OIDCプロバイダ(OP)にリダイレクトする 3. RPは、認証コード、IDトークン、JWTアク セス・トークンなどをOPと交換する 4. アプリは、別アプリ(アプリ1)にリクエス トを送る(JWTが伝搬される) 5. アプリ1をホストしているサーバーがJWTを評 価し、サブジェクトとJWTを作成 6. アプリ1をホストしているサーバーはJWTを認 可して、JWTを使ってアプリ2を呼び出し 7. . アプリ2をホストしているサーバーがJWTを 評価し、サブジェクトとJWTを作成 32
  26. JWTトークン使⽤のシステム事例 User A モバイルデバイス ピーク時には ⼀分あたり 14000 リクエスト アクセスマネージャー •

    ログイン認証 • JWT トークンを作成 リクエストは契約タイプに よって4つのFunctionalID にマップされる User B User C User D User A 認証キャッシュ User B User C User D ユーザーレジストリ ユーザー検証は • トークン期限切れ • キャッシュタイム アウト キャッシュでスルー プットを改善 アプリケーションサーバー
  27. デモのプログラム(MicroProfile starter) Demo OpenLiberty MP 5.0 Demo Quarkus MP 3.2

    start.microprofile.ioから、デモプログラムをダウンロードします。 異なるランタイム、異なるMicroProfileバージョンでのJWTの認証を実際に⾒てみます。
  28. デモの流れ ブラウザー Service-a localhost:8080 Quarkus Service-b localhost:8180 OpenLiberty JWT グループ値

    カスタム値 アクセスの結果を返す 0. Quarkus上で、Service-aを起動、OpenLiberty上で、Service-bを起動する 1. ブラウザーから、デモ⽤のサンプル起動⽤アプリを開く http://localhost:8080/ 2. アプリからService-aのエンドポイントにアクセスする http://localhost:8080/data/secured/test 3. Service-aのエンドポイントで、JWTトークンを作成、セキュアなService-bのエンドポイントにリクエスト送信 http://localhost:8180/data/protected 4. Service-bは、JWTトークンを評価して、アクセス権限がある時のみ、トークンのカスタム値をService-aに返す 5. Service-aは、結果をブラウザに表⽰ アクセスの結果を返す
  29. デモのノート • デモで使⽤したMicroProfileのStarterのサンプルは、簡単に動いて勉強に お勧めです︕ • 違うランタイム間で動かすときは、ポートを変える必要があるかもしれません • Quarkusのサービスbは、ポート8180動いていました • OpenLibertyのサービスbは、ポート9080をListenしていました

    • OpenLibertyの構成を、9080から8180をListenするように変えました • コードと構成はGithubにあります https://github.com/una-tapa/MicroProfileDemo OpenLibertyのserver.xml <httpEndpoint id="defaultHttpEndpoint" httpPort=“8180” <!– もとは9080 --> httpsPort="9444"/>
  30. 41

  31. @Path("/client-test1") public class ClientTestDefault { @GET @Produces(MediaType.TEXT_PLAIN) public String ping()

    { Client client = ClientBuilder.newBuilder().build(); WebTarget webTarget = client .target("http://localhost:9081/endpoint"); String output = webTarget.request().get(String.class); client.close(); return output; } } 左のコードは、JakartaのRestfulクライアントか らサーバーにリクエストを送るコードです。サン プルにも同様のコードがありました。 ClientBuilderで、送信先のWebTargetを作成して リクエストを送っています。 Jakarta RESTful クライアントのパフォーマンス改善
  32. @Path("/client-test2") public class ClientTestCached { private static WebTarget cachedWebTarget =

    ClientBuilder.newBuilder().build() .target("http://localhost:9081/endpoint"); @GET @Produces(MediaType.TEXT_PLAIN) public String ping() { return cachedWebTarget.request().get(String.class); } } パフォーマンス裏技 記事(⽇本語) https://community.ibm.com/community/user/wasdevops/blogs/kaori- asada/2022/08/17/microservices?CommunityKey=d6c93aa2-6e10-48da-96dc-3831da8ee185 前のページと、コードがよく似ていますが、 WebTarget をstaticで作ると、再利⽤でき、ク ライアントを閉じる必要がありません。 これだけの違いで、JakartaのRESTfulクライア ントのスループットが3倍になったそうです。 Jakarta RESTful クライアントのパフォーマンス改善