1. 概要

このチュートリアルでは、JakartaEEとMicroProfileを使用したOAuth2.0認証フレームワークの実装を提供します。 最も重要なことは、OAuth2.0の役割認証コード付与タイプの相互作用を実装することです。 この記事の背後にある動機は、Jakarta EEを使用して実装されたプロジェクトをサポートすることです。これは、OAuthのサポートをまだ提供していないためです。

最も重要な役割である承認サーバーには、承認エンドポイント、トークンエンドポイント、さらにJWKキーエンドポイントを実装します。これは、リソースサーバーが公開キーを取得するのに役立ちます。

実装をシンプルで簡単にセットアップできるようにしたいので、事前に登録されたクライアントとユーザーのストア、そして明らかにアクセストークン用のJWTストアを使用します。

トピックに飛び込む前に、このチュートリアルの例は教育目的であることに注意することが重要です。 本番システムの場合、Keycloakなどの十分にテストされた成熟したソリューションを使用することを強くお勧めします。

2. OAuth2.0の概要

このセクションでは、OAuth2.0の役割と認証コードの付与フローの概要を説明します。

2.1. 役割

OAuth 2.0フレームワークは、次の4つの役割間のコラボレーションを意味します。

  • リソース所有者:通常、これはエンドユーザーです–保護する価値のあるリソースを持っているエンティティです
  • リソースサーバー:リソース所有者のデータを保護するサービスで、通常はRESTAPIを介して公開します
  • クライアント:リソース所有者のデータを使用するアプリケーション
  • 承認サーバー:期限切れのトークンの形式でクライアントに権限(または権限)を付与するアプリケーション

2.2. 承認付与タイプ

付与タイプは、クライアントがリソース所有者のデータを使用するためのアクセス許可を取得する方法であり、最終的にはアクセストークンの形式になります。

当然、さまざまなタイプのクライアントはさまざまなタイプの助成金を優先します

  • 認証コード最も頻繁に優先される Webアプリケーション、ネイティブアプリケーション、またはシングルページアプリケーションのいずれであってもネイティブアプリとシングルページアプリには、PKCEと呼ばれる追加の保護が必要です
  • 更新トークン:特別な更新助成金、既存のトークンを更新するためのWebアプリケーションに適しています
  • クライアント資格情報サービス間通信に適しています。たとえば、リソース所有者がエンドユーザーでない場合などです。
  • リソース所有者パスワード:ネイティブアプリケーションのファーストパーティ認証に適しています。モバイルアプリに独自のログインページが必要な場合などです。

さらに、クライアントはimplicit付与タイプを使用できます。 ただし、通常は、PKCEで認証コード付与を使用する方が安全です。

2.3. 承認コード付与フロー

認証コードの付与フローが最も一般的であるため、それがどのように機能するかについても確認しましょう。これが実際にこのチュートリアルで作成するものです。

アプリケーション–クライアント– は、承認サーバーの/ authorizeエンドポイントにリダイレクトすることで許可を要求します。このエンドポイントに対して、アプリケーションはコールバックエンドポイントを提供します。

承認サーバーは通常、エンドユーザー(リソース所有者)に許可を求めます。 エンドユーザーが権限を付与すると、承認サーバーはコードを使用してコールバックにリダイレクトします。

アプリケーションはこのコードを受け取り、次に許可サーバーの/tokenエンドポイントに対して認証された呼び出しを行います。 「認証済み」とは、アプリケーションがこの呼び出しの一部として誰であるかを証明することを意味します。 すべてが順番に表示される場合、許可サーバーはトークンで応答します。

トークンが手元にあると、アプリケーションはAPI (リソースサーバー)にリクエストを送信し、そのAPIがトークンを検証します。 /introspectエンドポイントを使用してトークンを検証するように承認サーバーに要求できます。 または、トークンが自己完結型の場合、JWTの場合のように、リソースサーバーはトークンの署名をローカルで検証することで最適化できます。

2.4. Jakarta EEは何をサポートしていますか?

まだ多くはありません。 このチュートリアルでは、ほとんどのものをゼロから構築します。

3. OAuth2.0認証サーバー

この実装では、最も一般的に使用される付与タイプ:認証コードに焦点を当てます。

3.1. クライアントとユーザーの登録

もちろん、承認サーバーは、要求を承認する前に、クライアントとユーザーについて知る必要があります。 また、承認サーバーにはこのためのUIがあるのが一般的です。

ただし、簡単にするために、事前構成されたクライアントを使用します。

INSERT INTO clients (client_id, client_secret, redirect_uri, scope, authorized_grant_types) 
VALUES ('webappclient', 'webappclientsecret', 'http://localhost:9180/callback', 
  'resource.read resource.write', 'authorization_code refresh_token');
@Entity
@Table(name = "clients")
public class Client {
    @Id
    @Column(name = "client_id")
    private String clientId;
    @Column(name = "client_secret")
    private String clientSecret;

    @Column(name = "redirect_uri")
    private String redirectUri;

    @Column(name = "scope")
    private String scope;

    // ...
}

そして、事前設定されたユーザー:

INSERT INTO users (user_id, password, roles, scopes)
VALUES ('appuser', 'appusersecret', 'USER', 'resource.read resource.write');
@Entity
@Table(name = "users")
public class User implements Principal {
    @Id
    @Column(name = "user_id")
    private String userId;

    @Column(name = "password")
    private String password;

    @Column(name = "roles")
    private String roles;

    @Column(name = "scopes")
    private String scopes;

    // ...
}

このチュートリアルでは、プレーンテキストでパスワードを使用しましたが、実稼働環境では、パスワードをハッシュする必要があることに注意してください

このチュートリアルの残りの部分では、 appuser – リソース所有者–が、認証コードを実装することにより、 webappclient –アプリケーションへのアクセスを許可する方法を示します。

3.2. 承認エンドポイント

承認エンドポイントの主な役割は、最初にユーザーを認証してから、アプリケーションが必要とする権限(またはスコープ)を要求することです。

OAuth2仕様で指示されているように、このエンドポイントはHTTP GETメソッドをサポートする必要がありますが、HTTPPOSTメソッドもサポートできます。 この実装では、HTTPGETメソッドのみをサポートします。

まず、承認エンドポイントでは、ユーザーが認証されている必要があります。 ここでは仕様に特定の方法は必要ないため、 JakartaEE8セキュリティAPIのフォーム認証を使用してみましょう。

@FormAuthenticationMechanismDefinition(
  loginToContinue = @LoginToContinue(loginPage = "/login.jsp", errorPage = "/login.jsp")
)

ユーザーは認証のために/login.jspにリダイレクトされ、S ecurityContextAPIを介してCallerPrincipalとして使用できるようになります。

Principal principal = securityContext.getCallerPrincipal();

JAX-RSを使用してこれらをまとめることができます。

@FormAuthenticationMechanismDefinition(
  loginToContinue = @LoginToContinue(loginPage = "/login.jsp", errorPage = "/login.jsp")
)
@Path("authorize")
public class AuthorizationEndpoint {
    //...    
    @GET
    @Produces(MediaType.TEXT_HTML)
    public Response doGet(@Context HttpServletRequest request,
      @Context HttpServletResponse response,
      @Context UriInfo uriInfo) throws ServletException, IOException {
        
        MultivaluedMap<String, String> params = uriInfo.getQueryParameters();
        Principal principal = securityContext.getCallerPrincipal();
        // ...
    }
}

この時点で、承認エンドポイントはアプリケーションのリクエストの処理を開始できます。このリクエストには、 response_typeおよびclient_idパラメーターと、オプションで、ただし推奨される、redirect_uri、scope、およびstateパラメーターが含まれている必要があります。

client_id は、この場合はclientsデータベーステーブルからの有効なクライアントである必要があります。

redirect_uri は、指定されている場合、クライアントデータベーステーブルにあるものとも一致する必要があります。

そして、認証コードを実行しているので、 response_type はコード。 

承認は複数のステップからなるプロセスであるため、これらの値をセッションに一時的に保存できます。

request.getSession().setAttribute("ORIGINAL_PARAMS", params);

次に、アプリケーションが使用できる権限をユーザーに尋ねて、そのページにリダイレクトする準備をします。

String allowedScopes = checkUserScopes(user.getScopes(), requestedScope);
request.setAttribute("scopes", allowedScopes);
request.getRequestDispatcher("/authorize.jsp").forward(request, response);

3.3. ユーザースコープの承認

この時点で、ブラウザはユーザーの認証UIをレンダリングし、ユーザーが選択を行います。次に、ブラウザでユーザーの選択をHTTPPOSTに送信します。 ]:

@POST
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
@Produces(MediaType.TEXT_HTML)
public Response doPost(@Context HttpServletRequest request, @Context HttpServletResponse response,
  MultivaluedMap<String, String> params) throws Exception {
    MultivaluedMap<String, String> originalParams = 
      (MultivaluedMap<String, String>) request.getSession().getAttribute("ORIGINAL_PARAMS");

    // ...

    String approvalStatus = params.getFirst("approval_status"); // YES OR NO

    // ... if YES

    List<String> approvedScopes = params.get("scope");

    // ...
}

次に、 user_id、client_id、および redirect_uriを参照する一時コードを生成します。これらはすべて、アプリケーションが後でトークンエンドポイントに到達したときに使用します。

それでは、自動生成されたID を使用して AuthorizationCodeJPAエンティティを作成しましょう。

@Entity
@Table(name ="authorization_code")
public class AuthorizationCode {
@Id
@GeneratedValue(strategy=GenerationType.AUTO)
@Column(name = "code")
private String code;

//...

}

そして、それを入力します。

AuthorizationCode authorizationCode = new AuthorizationCode();
authorizationCode.setClientId(clientId);
authorizationCode.setUserId(userId);
authorizationCode.setApprovedScopes(String.join(" ", authorizedScopes));
authorizationCode.setExpirationDate(LocalDateTime.now().plusMinutes(2));
authorizationCode.setRedirectUri(redirectUri);

Beanを保存すると、コード属性が自動入力されるため、Beanを取得してクライアントに送り返すことができます。

appDataRepository.save(authorizationCode);
String code = authorizationCode.getCode();

認証コードは2分で期限切れになることに注意してください–この有効期限でできる限り保守的にする必要があります。 クライアントがすぐにアクセストークンと交換するため、短くなる可能性があります。

次に、アプリケーションの redirect_uriにリダイレクトし、にコードと、アプリケーションが /authorizeリクエストで指定したstateパラメーターを指定します。

StringBuilder sb = new StringBuilder(redirectUri);
// ...

sb.append("?code=").append(code);
String state = params.getFirst("state");
if (state != null) {
    sb.append("&state=").append(state);
}
URI location = UriBuilder.fromUri(sb.toString()).build();
return Response.seeOther(location).build();

redirectUriは、redirect_uri要求パラメーターではなく、clientsテーブルに存在するものであることに再度注意してください。

したがって、次のステップは、クライアントがこのコードを受け取り、トークンエンドポイントを使用してアクセストークンと交換することです。

3.4. トークンエンドポイント

承認エンドポイントとは対照的に、トークンエンドポイントはクライアントと通信するためにブラウザーを必要としないため、JAX-RSエンドポイントとして実装します。

@Path("token")
public class TokenEndpoint {

    List<String> supportedGrantTypes = Collections.singletonList("authorization_code");

    @Inject
    private AppDataRepository appDataRepository;

    @Inject
    Instance<AuthorizationGrantTypeHandler> authorizationGrantTypeHandlers;

    @POST
    @Produces(MediaType.APPLICATION_JSON)
    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
    public Response token(MultivaluedMap<String, String> params,
       @HeaderParam(HttpHeaders.AUTHORIZATION) String authHeader) throws JOSEException {
        //...
    }
}

トークンエンドポイントには、POSTと、 application /x-www-form-urlencodedメディアタイプを使用したパラメーターのエンコードが必要です。

すでに説明したように、認証コードの付与タイプのみをサポートします。

List<String> supportedGrantTypes = Collections.singletonList("authorization_code");

したがって、必須パラメーターとして受け取ったgrant_typeをサポートする必要があります。

String grantType = params.getFirst("grant_type");
Objects.requireNonNull(grantType, "grant_type params is required");
if (!supportedGrantTypes.contains(grantType)) {
    JsonObject error = Json.createObjectBuilder()
      .add("error", "unsupported_grant_type")
      .add("error_description", "grant type should be one of :" + supportedGrantTypes)
      .build();
    return Response.status(Response.Status.BAD_REQUEST)
      .entity(error).build();
}

次に、HTTP基本認証を介してクライアント認証を確認します。 つまり、をチェックして、受信したclient_idとclient_secret Authorization ヘッダーを介して、が登録済みクライアントと一致するかどうかを確認します:

String[] clientCredentials = extract(authHeader);
String clientId = clientCredentials[0];
String clientSecret = clientCredentials[1];
Client client = appDataRepository.getClient(clientId);
if (client == null || clientSecret == null || !clientSecret.equals(client.getClientSecret())) {
    JsonObject error = Json.createObjectBuilder()
      .add("error", "invalid_client")
      .build();
    return Response.status(Response.Status.UNAUTHORIZED)
      .entity(error).build();
}

最後に、TokenResponseの生成を対応する付与タイプハンドラーに委任します。

public interface AuthorizationGrantTypeHandler {
    TokenResponse createAccessToken(String clientId, MultivaluedMap<String, String> params) throws Exception;
}

認証コードの付与タイプに関心があるため、CDI beanとして適切な実装を提供し、Namedアノテーションで装飾しました。

@Named("authorization_code")

実行時に、受信した grant_type 値に従って、対応する実装がCDIインスタンスメカニズムを介してアクティブ化されます。

String grantType = params.getFirst("grant_type");
//...
AuthorizationGrantTypeHandler authorizationGrantTypeHandler = 
  authorizationGrantTypeHandlers.select(NamedLiteral.of(grantType)).get();

次に、 /tokenの応答を生成します。

3.5. RSA秘密鍵と公開鍵

トークンを生成する前に、トークンに署名するためのRSA秘密鍵が必要です。

この目的のために、OpenSSLを使用します。

# PRIVATE KEY
openssl genpkey -algorithm RSA -out private-key.pem -pkeyopt rsa_keygen_bits:2048

private-key.pem は、ファイル META-INF / microprofile-config.properties:を使用してMicroProfile Config signingKeyプロパティを介してサーバーに提供されます。

signingkey=/META-INF/private-key.pem

サーバーは、挿入されたConfigオブジェクトを使用してプロパティを読み取ることができます。

String signingkey = config.getValue("signingkey", String.class);

同様に、対応する公開鍵を生成できます。

# PUBLIC KEY
openssl rsa -pubout -in private-key.pem -out public-key.pem

そして、MicroProfile Config verificationKeyを使用してそれを読み取ります。

verificationkey=/META-INF/public-key.pem

サーバーは、次のリソースサーバーで使用できるようにする必要があります。 検証の目的。 これは行われます JWKエンドポイントを介して。

Nimbus JOSE + JWT は、ここで大きな助けとなるライブラリです。 まず、nimbus-jose-jwt依存関係を追加しましょう。

<dependency>
    <groupId>com.nimbusds</groupId>
    <artifactId>nimbus-jose-jwt</artifactId>
    <version>7.7</version>
</dependency>

そして今、NimbusのJWKサポートを活用して、エンドポイントを簡素化できます。

@Path("jwk")
@ApplicationScoped
public class JWKEndpoint {

    @GET
    public Response getKey(@QueryParam("format") String format) throws Exception {
        //...

        String verificationkey = config.getValue("verificationkey", String.class);
        String pemEncodedRSAPublicKey = PEMKeyUtils.readKeyAsString(verificationkey);
        if (format == null || format.equals("jwk")) {
            JWK jwk = JWK.parseFromPEMEncodedObjects(pemEncodedRSAPublicKey);
            return Response.ok(jwk.toJSONString()).type(MediaType.APPLICATION_JSON).build();
        } else if (format.equals("pem")) {
            return Response.ok(pemEncodedRSAPublicKey).build();
        }

        //...
    }
}

パラメータの形式を使用して、PEM形式とJWK形式を切り替えました。 リソースサーバーの実装に使用するMicroProfileJWTは、これらの両方の形式をサポートしています。

3.6. トークンエンドポイント応答

次に、特定のAuthorizationGrantTypeHandlerがトークン応答を作成します。 この実装では、構造化されたJWTトークンのみをサポートします。

この形式でトークンを作成するために、ここでもNimbus JOSE + JWTライブラリを使用しますが、他にも多数のJWTライブラリがあります。

したがって、署名されたJWTを作成するには、最初にJWTヘッダーを作成する必要があります:

JWSHeader jwsHeader = new JWSHeader.Builder(JWSAlgorithm.RS256).type(JOSEObjectType.JWT).build();

次に、標準化されたカスタムクレームのセットであるペイロードを構築します。

Instant now = Instant.now();
Long expiresInMin = 30L;
Date in30Min = Date.from(now.plus(expiresInMin, ChronoUnit.MINUTES));

JWTClaimsSet jwtClaims = new JWTClaimsSet.Builder()
  .issuer("http://localhost:9080")
  .subject(authorizationCode.getUserId())
  .claim("upn", authorizationCode.getUserId())
  .audience("http://localhost:9280")
  .claim("scope", authorizationCode.getApprovedScopes())
  .claim("groups", Arrays.asList(authorizationCode.getApprovedScopes().split(" ")))
  .expirationTime(in30Min)
  .notBeforeTime(Date.from(now))
  .issueTime(Date.from(now))
  .jwtID(UUID.randomUUID().toString())
  .build();
SignedJWT signedJWT = new SignedJWT(jwsHeader, jwtClaims);

標準のJWTクレームに加えて、MicroProfile JWTで必要とされるため、upngroupsの2つのクレームを追加しました。 upnはJakartaEESecurity CallerPrincipal にマップされ、groupsはJakartaEERolesにマップされます。

ヘッダーとペイロードができたので、RSA秘密鍵を使用してアクセストークンに署名する必要があります。 対応するRSA公開鍵は、JWKエンドポイントを介して公開されるか、他の手段で利用可能になり、リソースサーバーがそれを使用してアクセストークンを検証できるようになります。

秘密鍵をPEM形式で提供したので、それを取得して RSAPrivateKey:に変換する必要があります。

SignedJWT signedJWT = new SignedJWT(jwsHeader, jwtClaims);
//...
String signingkey = config.getValue("signingkey", String.class);
String pemEncodedRSAPrivateKey = PEMKeyUtils.readKeyAsString(signingkey);
RSAKey rsaKey = (RSAKey) JWK.parseFromPEMEncodedObjects(pemEncodedRSAPrivateKey);

次に、 JWTに署名してシリアル化します:

signedJWT.sign(new RSASSASigner(rsaKey.toRSAPrivateKey()));
String accessToken = signedJWT.serialize();

そして最後にトークン応答を作成します:

return Json.createObjectBuilder()
  .add("token_type", "Bearer")
  .add("access_token", accessToken)
  .add("expires_in", expiresInMin * 60)
  .add("scope", authorizationCode.getApprovedScopes())
  .build();

これは、JSON-Pのおかげで、JSON形式にシリアル化され、クライアントに送信されます。

{
  "access_token": "acb6803a48114d9fb4761e403c17f812",
  "token_type": "Bearer",  
  "expires_in": 1800,
  "scope": "resource.read resource.write"
}

4. OAuth2.0クライアント

このセクションでは、サーブレット、MicroProfile Config、およびJAXRSクライアントAPIを使用してWebベースのOAuth2.0クライアントを構築します。

より正確には、2つの主要なサーブレットを実装します。1つは認証サーバーの認証エンドポイントを要求して認証コード付与タイプを使用してコードを取得するためのもので、もう1つは受信したコードを使用して認証サーバーのトークンエンドポイントからアクセストークンを要求するためのものです。 。

さらに、さらに2つのサーブレットを実装します。1つは更新トークン付与タイプを使用して新しいアクセストークンを取得するためのもので、もう1つはリソースサーバーのAPIにアクセスするためのものです。

4.1. OAuth2.0クライアントの詳細

クライアントはすでに認証サーバーに登録されているため、最初にクライアント登録情報を提供する必要があります。

  • client_id:クライアント識別子。通常、登録プロセス中に認証サーバーによって発行されます。
  • client_secret:クライアントシークレット。
  • redirect_uri:認証コードを受け取る場所。
  • スコープ:クライアントが権限を要求しました。

さらに、クライアントは承認サーバーの承認とトークンのエンドポイントを知っている必要があります。

  • authentication_uri:コードの取得に使用できる承認サーバー承認エンドポイントの場所。
  • token_uri:トークンの取得に使用できる承認サーバートークンエンドポイントの場所。

このすべての情報は、MicroProfileConfigファイルMETA-INF / microprofile-config.properties:を通じて提供されます。

# Client registration
client.clientId=webappclient
client.clientSecret=webappclientsecret
client.redirectUri=http://localhost:9180/callback
client.scope=resource.read resource.write

# Provider
provider.authorizationUri=http://127.0.0.1:9080/authorize
provider.tokenUri=http://127.0.0.1:9080/token

4.2. 認証コードリクエスト

認証コードを取得するフローは、ブラウザを認証サーバーの認証エンドポイントにリダイレクトすることにより、クライアントから始まります。

通常、これは、ユーザーが許可なしに、またはクライアント / authorize パスを明示的に呼び出して、保護されたリソースAPIにアクセスしようとしたときに発生します。

@WebServlet(urlPatterns = "/authorize")
public class AuthorizationCodeServlet extends HttpServlet {

    @Inject
    private Config config;

    @Override
    protected void doGet(HttpServletRequest request, 
      HttpServletResponse response) throws ServletException, IOException {
        //...
    }
}

doGet()メソッドでは、セキュリティ状態値を生成して保存することから始めます。

String state = UUID.randomUUID().toString();
request.getSession().setAttribute("CLIENT_LOCAL_STATE", state);

次に、クライアント構成情報を取得します。

String authorizationUri = config.getValue("provider.authorizationUri", String.class);
String clientId = config.getValue("client.clientId", String.class);
String redirectUri = config.getValue("client.redirectUri", String.class);
String scope = config.getValue("client.scope", String.class);

次に、これらの情報をクエリパラメータとして承認サーバーの承認エンドポイントに追加します。

String authorizationLocation = authorizationUri + "?response_type=code"
  + "&client_id=" + clientId
  + "&redirect_uri=" + redirectUri
  + "&scope=" + scope
  + "&state=" + state;

そして最後に、ブラウザを次のURLにリダイレクトします。

response.sendRedirect(authorizationLocation);

リクエストを処理した後、 承認サーバーの承認エンドポイントがコードを生成して追加します 、受信した状態パラメータに加えて、 redirect_uri ブラウザにリダイレクトします http:// localhost:9081 / callback?code = A123&state = Y

4.3. アクセストークンリクエスト

クライアントコールバックサーブレット/callback、は、受信した状態を検証することから始まります:

String localState = (String) request.getSession().getAttribute("CLIENT_LOCAL_STATE");
if (!localState.equals(request.getParameter("state"))) {
    request.setAttribute("error", "The state attribute doesn't match!");
    dispatch("/", request, response);
    return;
}

次に、以前に受信したコードを使用して、認証サーバーのトークンエンドポイントを介してアクセストークンをリクエストします。

String code = request.getParameter("code");
Client client = ClientBuilder.newClient();
WebTarget target = client.target(config.getValue("provider.tokenUri", String.class));

Form form = new Form();
form.param("grant_type", "authorization_code");
form.param("code", code);
form.param("redirect_uri", config.getValue("client.redirectUri", String.class));

TokenResponse tokenResponse = target.request(MediaType.APPLICATION_JSON_TYPE)
  .header(HttpHeaders.AUTHORIZATION, getAuthorizationHeaderValue())
  .post(Entity.entity(form, MediaType.APPLICATION_FORM_URLENCODED_TYPE), TokenResponse.class);

ご覧のとおり、この呼び出しにはブラウザの操作はなく、リクエストはJAX-RSクライアントAPIをHTTPPOSTとして使用して直接行われます。

トークンエンドポイントにはクライアント認証が必要なため、Authorizationヘッダーにクライアントクレデンシャルclient_idclient_secretを含めました。

クライアントはこのアクセストークンを使用して、次のサブセクションの主題であるリソースサーバーAPIを呼び出すことができます。

4.4. 保護されたリソースアクセス

この時点で、有効なアクセストークンがあり、リソースサーバーの/ readおよび/writeAPIを呼び出すことができます。

そのためには、Authorizationヘッダーを提供する必要があります。 JAX-RSクライアントAPIを使用すると、これは Invocation.Builder header()メソッドを介して簡単に実行されます。

resourceWebTarget = webTarget.path("resource/read");
Invocation.Builder invocationBuilder = resourceWebTarget.request();
response = invocationBuilder
  .header("authorization", tokenResponse.getString("access_token"))
  .get(String.class);

5. OAuth2.0リソースサーバー

このセクションでは、JAX-RS、MicroProfile JWT、およびMicroProfileConfigに基づいてセキュリティで保護されたWebアプリケーションを構築します。 MicroProfile JWTは、受信したJWTの検証と、JWTスコープのJakartaEEロールへのマッピングを処理します

5.1. Mavenの依存関係

Java EE Web API の依存関係に加えて、 MicroProfileConfigおよびMicroProfileJWTAPIも必要です。

<dependency>
    <groupId>javax</groupId>
    <artifactId>javaee-web-api</artifactId>
    <version>8.0</version>
    <scope>provided</scope>
</dependency>
<dependency>
    <groupId>org.eclipse.microprofile.config</groupId>
    <artifactId>microprofile-config-api</artifactId>
    <version>1.3</version>
</dependency>
<dependency>
    <groupId>org.eclipse.microprofile.jwt</groupId>
    <artifactId>microprofile-jwt-auth-api</artifactId>
    <version>1.1</version>
</dependency>

5.2. JWT認証メカニズム

MicroProfile JWTは、ベアラートークン認証メカニズムの実装を提供します。 これにより、 Authorization ヘッダーに存在するJWTの処理が処理され、JWTクレームを保持する JsonWebToken としてJakartaEEセキュリティプリンシパルが利用可能になり、スコープがJakartaEEロールにマップされます。 詳細については、 Jakarta EE SecurityAPIをご覧ください。

サーバーでJWT認証メカニズムを有効にするには、JAX-RSアプリケーションにLoginConfigアノテーションを追加する必要があります。

@ApplicationPath("/api")
@DeclareRoles({"resource.read", "resource.write"})
@LoginConfig(authMethod = "MP-JWT")
public class OAuth2ResourceServerApplication extends Application {
}

さらに、 MicroProfile JWTは、JWT署名を検証するためにRSA公開鍵を必要とします。 これは、イントロスペクションによって、または簡単にするために、認証サーバーからキーを手動でコピーすることによって提供できます。 いずれの場合も、公開鍵の場所を提供する必要があります。

mp.jwt.verify.publickey.location=/META-INF/public-key.pem

最後に、MicroProfile JWTは、着信JWTの iss クレームを検証する必要があります。これは、存在し、MicroProfileConfigプロパティの値と一致する必要があります。

mp.jwt.verify.issuer=http://127.0.0.1:9080

通常、これは承認サーバーの場所です。

5.3. 保護されたエンドポイント

デモンストレーションの目的で、2つのエンドポイントを持つリソースAPIを追加します。 1つは[X7X]resource.readスコープを持つユーザーがアクセスできるreadエンドポイントで、もう1つはresource.writeスコープを持つユーザー用のwriteエンドポイントです。 。

スコープの制限は、@RolesAllowedアノテーションを介して行われます。

@Path("/resource")
@RequestScoped
public class ProtectedResource {

    @Inject
    private JsonWebToken principal;

    @GET
    @RolesAllowed("resource.read")
    @Path("/read")
    public String read() {
        return "Protected Resource accessed by : " + principal.getName();
    }

    @POST
    @RolesAllowed("resource.write")
    @Path("/write")
    public String write() {
        return "Protected Resource accessed by : " + principal.getName();
    }
}

6. すべてのサーバーの実行

1つのサーバーを実行するには、対応するディレクトリでMavenコマンドを呼び出す必要があります。

mvn package liberty:run-server

承認サーバー、クライアント、およびリソースサーバーは、それぞれ次の場所で実行され、使用可能になります。

# Authorization Server
http://localhost:9080/

# Client
http://localhost:9180/

# Resource Server
http://localhost:9280/

したがって、クライアントのホームページにアクセスしてから、[アクセストークンの取得]をクリックして認証フローを開始します。 アクセストークンを受け取った後、リソースサーバーの読み取りおよび書き込みAPIにアクセスできます。

付与されたスコープに応じて、リソースサーバーは成功メッセージで応答するか、HTTP403禁止ステータスを取得します。

7. 結論

この記事では、互換性のあるOAuth2.0クライアントおよびリソースサーバーで使用できるOAuth2.0認証サーバーの実装を提供しました。

フレームワーク全体を説明するために、クライアントとリソースサーバーの実装も提供しました。 これらすべてのコンポーネントを実装するために、Jakarta EE 8 API、特にCDI、サーブレット、JAX RS、JakartaEEセキュリティを使用しました。 さらに、MicroProfileの疑似Jakarta EEAPIであるMicroProfileConfigとMicroProfileJWTを使用しました。

例の完全なソースコードは、GitHubから入手できます。 コードには、認証コードと更新トークン付与タイプの両方の例が含まれていることに注意してください。

最後に、この記事の教育的性質を認識し、与えられた例を実動システムで使用してはならないことを認識することが重要です。