Java EEを使用したOAuth 2.0承認フレームワークの実装

  • Java EE

  • link:/category/security-2/ [セキュリティ]

  • OAuth

1. 概要

このチュートリアルでは、Java EEおよびMicroProfileを使用してhttps://tools.ietf.org/html/rfc6749[OAuth 2.0 Authorization Framework]の実装を提供します。 最も重要なことは、https://tools.ietf.org/html/rfc6749を介してhttps://tools.ietf.org/html/rfc6749#page-6[OAuth 2.0の役割]の相互作用を実装することです。 #page-24 [認証コード付与タイプ]。 この記事の背後にある動機は、Java EEを使用して実装されるプロジェクトをサポートすることです。これはまだOAuthのサポートを提供していないためです。
最も重要な役割である承認サーバーについては、*承認エンドポイント、トークンエンドポイント、さらにはリソースサーバーが公開キーを取得するのに役立つJWKキーエンドポイント*を実装します。
迅速なセットアップのために実装をシンプルで簡単にしたいので、クライアントとユーザーの事前登録されたストア、そして明らかにアクセストークン用のJWTストアを使用します。

2. OAuth 2.0の概要

このセクションでは、OAuth 2.0の役割と承認コード付与フローの概要を簡単に説明します。

2.1. 役割

OAuth 2.0フレームワークは、次の4つのロール間のコラボレーションを意味します。
  • Resource Owner:通常、これはエンドユーザーです。つまり、エンティティです
    保護する価値のあるリソースがあります

  • Resource Server:リソース所有者のデータを保護するサービス、
    通常、REST APIを介して公開します

  • Client:リソース所有者のデータを使用するアプリケーション

  • Authorization Server:許可を与えるアプリケーション–または
    期限切れトークンの形でのクライアントに対する権限

2.2. 認可付与タイプ

A __grant type ___は、クライアントがリソース所有者のデータを使用する許可を取得する方法であり、最終的にはアクセストークンの形式です。
当然、さまざまなタイプのクライアントhttps://oauth2.thephpleague.com/authorization-server/which-grant/ [さまざまなタイプの許可を推奨]:
  • 承認コード最も頻繁に優先される –

  • Webアプリケーション、ネイティブアプリケーション、または単一ページアプリケーション*。ただし、ネイティブアプリケーションと単一ページアプリケーションには、PKCEと呼ばれる追加の保護が必要です。

  • Refresh Token:特別な更新許可、Webに適しています
    既存のトークンを更新するアプリケーション*

  • Client Credentialsサービス間で優先
    コミュニケーション
    、リソース所有者がエンドユーザーではない場合

  • Resource Owner Passwordファーストパーティに優先
    ネイティブアプリケーションの認証
    モバイルアプリに独自のログインページが必要な場合

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

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

承認コードの付与フローが最も一般的であるため、それがどのように機能するかを確認しましょう。*実際にこのチュートリアルで作成するものです*。
アプリケーション–クライアント– *許可サーバーの_ / authorize_エンドポイントにリダイレクトすることで許可を要求します。*このエンドポイントに、アプリケーションは_callback_エンドポイントを提供します。
承認サーバーは通常、エンドユーザー(リソース所有者)に許可を求めます。 エンドユーザーが許可を与えると、*承認サーバーはa__code_でコールバックにリダイレクトします*。
アプリケーションはこのコードを受け取り、**認証サーバーの__ / token ___endpointに対して認証された呼び出しを行います。 **「認証済み」とは、この呼び出しの一部としてアプリケーションが誰であるかを証明することを意味します。 すべてが順番に表示される場合、認可サーバーはトークンで応答します。
トークンを手にすると、*アプリケーションはAPI *(リソースサーバー)に要求を行い、そのAPIはトークンを検証します。 _ / introspect_エンドポイントを使用してトークンを検証するように承認サーバーに要求できます。 または、トークンが自己完結型である場合、リソースサーバーは、JWTの場合のように、トークンの署名をローカルで検証することにより最適化できます。

2.4. Java EEは何をサポートしますか?

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

3. OAuth 2.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. 承認エンドポイント

承認エンドポイントの主な役割は、最初に*ユーザーを認証してから、アプリケーションが必要とするアクセス許可*(またはスコープ)を要求することです。
https://tools.ietf.org/html/rfc6749#section-3.1[OAuth2仕様による指示]として、このエンドポイントはHTTP GETメソッドをサポートする必要がありますが、HTTP POSTメソッドもサポートできます。 この実装では、HTTP GETメソッドのみをサポートします。
最初に、*承認エンドポイントでは、ユーザーの認証が必要です*。 ここでの仕様は特定の方法を必要としないため、https://www.baeldung.com/java-ee-8-security [Java EE 8 Security API]のフォーム認証を使用しましょう。
@FormAuthenticationMechanismDefinition(
  loginToContinue = @LoginToContinue(loginPage = "/login.jsp", errorPage = "/login.jsp")
)
ユーザーは認証のために_ / login.jsp_にリダイレクトされ、S__ecurityContext__ APIを介して_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_が指定されている場合、_clients_データベーステーブルで見つかったものと一致する必要があります。
そして、承認コードを実行しているため、_response_type_は_codeです。 _
承認は多段階プロセスであるため、これらの値をセッションに一時的に保存できます。
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をレンダリングし、*ユーザーが選択を行います*。その後、ブラウザーは* HTTP POST *でユーザーの選択を送信します。
@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 __:__で_AuthorizationCode_ JPAエンティティを作成しましょう
@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を保存すると、コード属性が自動入力されるため、それを取得してクライアントに送り返すことができます。
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_メディアタイプを使用したパラメーターのエンコードが必要です。
先ほど説明したように、_authorization code_付与タイプのみをサポートします。
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_値に従って、対応する実装がhttps://javaee.github.io/javaee-spec/javadocs/javax/enterprise/inject/Instance.html[CDIインスタンスメカニズム]を介してアクティブ化されます。
String grantType = params.getFirst("grant_type");
//...
AuthorizationGrantTypeHandler authorizationGrantTypeHandler =
  authorizationGrantTypeHandlers.select(NamedLiteral.of(grantType)).get();
_ /token_‘sの応答を生成する時が来ました。

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エンドポイントを介して行われます。*
  • https://connect2id.com/products/nimbus-jose-jwt [Nimbus JOSE JWT] *は、ここで大きな助けになるライブラリです。 最初にhttps://search.maven.org/search?q=g:com.nimbusds%20AND%20a: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形式を切り替えるために、_parameter_という形式を使用しました。 リソースサーバーの実装に使用するMicroProfile JWTは、これらの両方の形式をサポートしています。

3.6. トークンエンドポイントレスポンス

与えられた_AuthorizationGrantTypeHandler_がトークン応答を作成する時が来ました。 この実装では、構造化されたJWTトークンのみをサポートします。
*この形式でトークンを作成するには、https://connect2id.com/products/nimbus-jose-jwt [Nimbus JOSE JWT]ライブラリ*を再度使用しますが、https://jwt.io/#がありますlibraries-io [その他の多数のJWTライブラリ]も。
そのため、署名付きJWTを作成するには、* JWTヘッダーを最初に作成する必要があります:*
JWSHeader jwsHeader = new JWSHeader.Builder(JWSAlgorithm.RS256).type(JOSEObjectType.JWT).build();
*次に、ペイロードを構築します。これは、標準化されたカスタムクレームの_Set_です:
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が必要とする2つのクレーム_upn_および_groups_が追加されました。 _upn_はJava EEセキュリティ_CallerPrincipal_にマップされ、_groups_はJava EE _Roles._にマップされます。
ヘッダーとペイロードが用意できたので、* 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. OAuth 2.0クライアント

このセクションでは、Servlet、MicroProfile Config、およびJAX RS Client APIを使用して* WebベースのOAuth 2.0クライアントを構築します。
より正確には、2つの主要なサーブレットを実装します。1つは承認サーバーの承認エンドポイントを要求し、承認コード付与タイプを使用してコードを取得するものです。 。
さらに、さらに2つのサーブレットを実装します。1つは、更新トークン許可タイプを使用して新しいアクセストークンを取得し、もう1つはリソースサーバーのAPIにアクセスします。

4.1. OAuth 2.0クライアントの詳細

クライアントはすでに承認サーバーに登録されているため、最初にクライアント登録情報を提供する必要があります。
  • _client_id:_クライアント識別子。通常は、
    登録プロセス中の認可サーバー。

  • _client_secret:_クライアントシークレット。

  • _redirect_uri:_認証コードを受け取る場所。

  • _scope:_クライアントが許可を要求しました。

    さらに、クライアントは承認サーバーの承認およびトークンエンドポイントを知っている必要があります。
  • _authorization_uri:_許可サーバーの場所
    コードを取得するために使用できる承認エンドポイント。

  • _token_uri:_認証サーバートークンエンドポイントの場所
    トークンを取得するために使用できます。

    この情報はすべて、MicroProfile Configファイル_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. 認証コードのリクエスト

*認可コードを取得するフローは、クライアントからブラウザを認可サーバーの認可エンドポイントにリダイレクトすることから始まります。*
通常、これは、ユーザーが認証なしで保護されたリソースAPIにアクセスしようとした場合、またはクライアント_ / authorize_パスを明示的に呼び出して発生した場合に発生します。
@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

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

クライアントコールバックサーブレット_ / callback、_は、受信した_state:_を検証することから始まります
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をHTTP POSTとして直接使用して行われます。
トークンエンドポイントではクライアント認証が必要なため、_Authorization_ヘッダーにクライアント資格情報_client_id_および_client_secret_を含めました。
クライアントはこのアクセストークンを使用して、次のサブセクションの主題であるリソースサーバーAPIを呼び出すことができます。

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

*この時点で、有効なアクセストークンがあり、リソースサーバーの/ _read_および/ _write_ APIを呼び出すことができます。*
そのためには、* 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. OAuth 2.0リソースサーバー

このセクションでは、JAX-RS、MicroProfile JWT、およびMicroProfile Configに基づいて保護されたWebアプリケーションを構築します。 * MicroProfile JWTは、受信したJWTを検証し、JWTスコープをJava EEロールにマッピングします*。

5.1. Mavenの依存関係

https://search.maven.org/search?q=g:javax%20AND%20a:javaee-web-apiに加えて
<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_としてJava EEセキュリティプリンシパルが使用可能になり、スコープがJava EEロールにマップされます。 詳細については、https://www.baeldung.com/java-ee-8-security [JAVA EE Security API]をご覧ください。
サーバーで* 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_クレームを検証する必要があります。これは存在し、MicroProfile Configプロパティの値と一致する必要があります。
mp.jwt.verify.issuer=http://127.0.0.1:9080
通常、これは承認サーバーの場所です。

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

デモンストレーションのために、2つのエンドポイントを持つリソースAPIを追加します。 1つは、_resource.read_スコープを持つユーザーがアクセスできる_read_エンドポイントと、_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/
そのため、クライアントのホームページにアクセスし、[アクセストークンの取得]をクリックして認証フローを開始します。 アクセストークンを受け取った後、リソースサーバーの_read_および_write_ APIにアクセスできます。
許可されたスコープに応じて、リソースサーバーは成功したメッセージで応答するか、HTTP 403禁止ステータスを取得します。

7. 結論

この記事では、互換性のあるOAuth 2.0クライアントおよびリソースサーバーで使用できるOAuth 2.0承認サーバーの実装を提供しました。
フレームワーク全体を説明するために、クライアントとリソースサーバーの実装も提供しました。 これらすべてのコンポーネントを実装するために、Java EE 8 API、特にCDI、サーブレット、JAX RS、JAVA EE Securityを使用しました。 さらに、MicroProfileの疑似Java EE API:MicroProfile ConfigおよびMicroProfile JWTを使用しました。
サンプルの完全なソースコードは、https://github.com/eugenp/tutorials/tree/master/oauth2-framework-impl [GitHub上]で入手できます。 コードには、承認コードと更新トークン付与タイプの両方の例が含まれていることに注意してください。