«1. Обзор

В этом руководстве мы собираемся предоставить реализацию для платформы авторизации OAuth 2.0 с использованием Jakarta EE и MicroProfile. Самое главное, мы собираемся реализовать взаимодействие ролей OAuth 2.0 через тип гранта Authorization Code. Мотивация написания этой статьи — оказать поддержку проектам, реализованным с использованием Jakarta EE, так как это еще не обеспечивает поддержку OAuth.

Для самой важной роли, сервера авторизации, мы собираемся реализовать конечную точку авторизации, конечную точку маркера и, кроме того, конечную точку ключа JWK, которая полезна для сервера ресурсов для получения открытого ключа.

Поскольку мы хотим, чтобы реализация была простой и легкой для быстрой настройки, мы собираемся использовать предварительно зарегистрированное хранилище клиентов и пользователей и, очевидно, хранилище JWT для токенов доступа.

Прежде чем перейти непосредственно к теме, важно отметить, что пример в этом руководстве предназначен для образовательных целей. Для производственных систем настоятельно рекомендуется использовать зрелое, хорошо протестированное решение, такое как Keycloak.

2. Обзор OAuth 2.0

В этом разделе мы собираемся дать краткий обзор ролей OAuth 2.0 и потока предоставления кода авторизации.

2.1. Роли

Платформа OAuth 2.0 подразумевает сотрудничество между четырьмя следующими ролями:

    Владелец ресурса: обычно это конечный пользователь — это объект, у которого есть некоторые ресурсы, которые стоит защищать. Сервер ресурсов: служба, которая защищает данные владельца ресурса, обычно публикуя их через REST API. Клиент: приложение, использующее данные владельца ресурса. Сервер авторизации: приложение, которое предоставляет разрешение — или полномочия — клиентам в виде токенов с истекающим сроком действия

2.2 . Типы грантов авторизации

Тип гранта — это то, как клиент получает разрешение на использование данных владельца ресурса, в конечном счете, в форме токена доступа.

Естественно, разные типы клиентов предпочитают разные типы грантов:

    Код авторизации: Предпочтителен чаще всего — будь то веб-приложение, нативное приложение или одностраничное приложение, пусть нативное и одностраничное. приложениям страниц требуется дополнительная защита под названием PKCE Refresh Token: специальный грант на продление, подходящий для веб-приложений для обновления их существующих токенов. Учетные данные клиента: предпочтительнее для связи между службами, например, когда владелец ресурса не является владельцем ресурса конечного пользователя. Пароль: предпочтителен для первой стороны аутентификации собственных приложений, например, когда мобильному приложению требуется собственная страница входа

Кроме того, клиент может использовать неявный тип предоставления. Однако обычно более безопасно использовать предоставление кода авторизации с PKCE.

2.3. Поток предоставления кода авторизации

Поскольку поток предоставления кода авторизации является наиболее распространенным, давайте также рассмотрим, как он работает, и именно его мы создадим в этом руководстве.

Приложение — клиент — запрашивает разрешение, перенаправляя его на конечную точку /authorize сервера авторизации. Этой конечной точке приложение предоставляет конечную точку обратного вызова.

Сервер авторизации обычно запрашивает разрешение у конечного пользователя — владельца ресурса. Если конечный пользователь дает разрешение, то сервер авторизации перенаправляет обратно на обратный вызов с кодом.

Приложение получает этот код, а затем выполняет аутентифицированный вызов конечной точки /token сервера авторизации. Под «аутентифицированным» мы подразумеваем, что приложение доказывает, кто оно есть, в рамках этого звонка. Если все в порядке, сервер авторизации отвечает токеном.

Имея токен на руках, приложение делает запрос к API — серверу ресурсов — и этот API проверяет токен. Он может попросить сервер авторизации проверить токен, используя его конечную точку /introspect. Или, если токен автономный, сервер ресурсов может оптимизировать его, локально проверив подпись токена, как в случае с JWT.

2.4. Что поддерживает Jakarta EE?

«Пока не так много. В этом уроке мы создадим большинство вещей с нуля.

3. Сервер авторизации OAuth 2.0

В этой реализации мы сосредоточимся на наиболее часто используемом типе гранта: коде авторизации.

3.1. Регистрация клиентов и пользователей

Сервер авторизации, конечно, должен знать о клиентах и ​​пользователях, прежде чем он сможет авторизовать их запросы. И сервер авторизации обычно имеет для этого пользовательский интерфейс.

Однако для простоты мы будем использовать предварительно настроенного клиента:

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. Конечная точка авторизации

Основная роль конечной точки авторизации заключается в том, чтобы сначала аутентифицировать пользователя, а затем запросить разрешения — или области — которые нужны приложению.

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

Согласно спецификациям OAuth2, эта конечная точка должна поддерживать метод HTTP GET, хотя она также может поддерживать метод HTTP POST. В этой реализации мы будем поддерживать только метод HTTP GET.

Principal principal = securityContext.getCallerPrincipal();

Во-первых, конечная точка авторизации требует аутентификации пользователя. Спецификация не требует здесь определенного способа, поэтому давайте воспользуемся аутентификацией с помощью форм из Jakarta EE 8 Security API:

@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();
        // ...
    }
}

Пользователь будет перенаправлен на /login.jsp для аутентификации, а затем будет доступен как CallerPrincipal через SecurityContext API:

Мы можем собрать их вместе, используя JAX-RS:

В этот момент конечная точка авторизации может начать обработку запроса приложения, который должен содержать параметры response_type и client_id и â – необязательно, но рекомендуется – параметры redirect_uri, scope и state.

client_id должен быть действительным клиентом, в нашем случае из таблицы базы данных клиентов.

Redirect_uri, если он указан, также должен соответствовать тому, что мы находим в таблице базы данных клиентов.

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

И, поскольку мы делаем код авторизации, response_type — это код.

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

Поскольку авторизация — это многоэтапный процесс, мы можем временно сохранить эти значения в сеансе:

И затем подготовиться, чтобы спросить пользователя, какие разрешения может использовать приложение, перенаправляя на эту страницу:

@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");

    // ...
}

3.3. Утверждение областей пользователя

На этом этапе браузер отображает пользовательский интерфейс авторизации для пользователя, и пользователь делает выбор. Затем браузер отправляет выбор пользователя в HTTP POST:

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

//...

}

Затем мы генерируем временный код, который ссылается на user_id, client_id и redirect_uri, все из которых приложение будет использовать позже, когда оно попадет в токен. конечная точка.

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

Итак, давайте создадим объект JPA AuthorizationCode с автоматически сгенерированным идентификатором:

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

А затем заполним его:

Когда мы сохраняем bean-компонент, атрибут кода заполняется автоматически, и так далее. мы можем получить его и отправить обратно клиенту:

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();

Обратите внимание, что срок действия нашего кода авторизации истекает через две минуты — мы должны быть настолько консервативны, насколько это возможно, с этим сроком действия. Он может быть коротким, так как клиент сразу его обменяет на токен доступа.

Затем мы перенаправляем обратно на redirect_uri приложения, передавая ему код, а также любые параметры состояния, которые приложение указало в своем запросе /authorize: не параметр запроса redirect_uri.

Итак, наш следующий шаг — клиент должен получить этот код и обменять его на токен доступа, используя конечную точку токена.

3.4. Конечная точка токена

@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 {
        //...
    }
}

В отличие от конечной точки авторизации, конечной точке токена не нужен браузер для связи с клиентом, поэтому мы реализуем ее как конечную точку JAX-RS:

Для конечной точки токена требуется POST, а также кодирование параметров с использованием типа носителя application/x-www-form-urlencoded.

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

«Как мы уже говорили, мы будем поддерживать только тип гранта кода авторизации:

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();
}

Итак, полученный grant_type в качестве обязательного параметра должен поддерживаться:

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();
}

Далее мы проверяем аутентификацию клиента через via Базовая HTTP-аутентификация. То есть мы проверяем, соответствуют ли полученные client_id и client_secret через заголовок Authorization зарегистрированному клиенту:

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

Наконец, мы делегируем создание TokenResponse соответствующему обработчику типа предоставления:

@Named("authorization_code")

Поскольку нас больше интересует тип предоставления кода авторизации, мы предоставили адекватную реализацию в виде компонента CDI и украсили его аннотацией Named:

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

Во время выполнения и в соответствии с полученным значением grant_type соответствующая реализация активируется через механизм экземпляра CDI:

Пришло время создать ответ /token.

3.5. Закрытый и открытый ключи RSA

Перед созданием токена нам нужен закрытый ключ RSA для подписи токенов.

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

Для этой цели мы будем использовать OpenSSL:

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

Private-key.pem предоставляется серверу через свойство signingKey MicroProfile Config с использованием файла META-INF/microprofile-config.properties:

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

Сервер может прочитать свойство, используя введенный объект конфигурации:

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

Аналогичным образом мы можем сгенерировать соответствующий открытый ключ:

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

И использовать ключ проверки MicroProfile Config для его чтения:

Сервер должен сделать его доступным для сервера ресурсов с целью проверки. Это делается через конечную точку JWK.

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

Nimbus JOSE+JWT — это библиотека, которая может здесь очень помочь. Давайте сначала добавим зависимость nimbus-jose-jwt:

@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();
        }

        //...
    }
}

И теперь мы можем использовать поддержку Nimbus JWK для упрощения нашей конечной точки:

Мы использовали параметр формата для переключения между PEM и форматы JWK. MicroProfile JWT, который мы будем использовать для реализации сервера ресурсов, поддерживает оба этих формата.

3.6. Ответ конечной точки маркера

Пришло время данному обработчику AuthorizationGrantTypeHandler создать ответ маркера. В этой реализации мы будем поддерживать только структурированные токены JWT.

Для создания токена в этом формате мы снова воспользуемся библиотекой Nimbus JOSE+JWT, но есть и множество других библиотек JWT.

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

Итак, чтобы создать подписанный JWT, мы сначала должны создать заголовок JWT:

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 мы добавили еще два утверждения — upn и groups — поскольку они необходимы JWT MicroProfile. Upn будет сопоставлен с Jakarta EE Security CallerPrincipal, а группы будут сопоставлены с ролями Jakarta EE.

Теперь, когда у нас есть заголовок и полезная нагрузка, нам нужно подписать токен доступа закрытым ключом RSA. Соответствующий открытый ключ RSA будет предоставлен через конечную точку JWK или сделан доступным другим способом, чтобы сервер ресурсов мог использовать его для проверки токена доступа.

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

Поскольку мы предоставили закрытый ключ в формате PEM, мы должны извлечь его и преобразовать в RSAPrivateKey:

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

Затем мы подписываем и сериализуем JWT:

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

И, наконец, мы создаем токен-ответ:

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

, который благодаря JSON-P сериализуется в формат JSON и отправляется клиенту:

4. Клиент OAuth 2.0

В этом разделе мы мы создадим веб-клиент OAuth 2.0 с использованием API-интерфейсов Servlet, MicroProfile Config и JAX RS Client.

Точнее, мы будем реализовывать два основных сервлета: один для запроса конечной точки авторизации сервера авторизации и получения кода с использованием типа гранта кода авторизации, а другой сервлет для использования полученного кода и запроса токена доступа из авторизации конечная точка токена сервера.

Кроме того, мы реализуем еще два сервлета: один для получения нового токена доступа с использованием типа предоставления токена обновления, а другой для доступа к API-интерфейсам сервера ресурсов.

4.1. Сведения о клиенте OAuth 2.0

    «Поскольку клиент уже зарегистрирован на сервере авторизации, нам сначала необходимо предоставить информацию о регистрации клиента:

client_id: идентификатор клиента, который обычно выдается сервером авторизации в процессе регистрации. client_secret: секрет клиента. redirect_uri: место, где можно получить код авторизации. область действия: запрошенные клиентом разрешения.

    Кроме того, клиент должен знать конечные точки авторизации и маркера сервера авторизации:

author_uri: расположение конечной точки авторизации сервера авторизации, которую мы можем использовать для получения кода. token_uri: расположение конечной точки токена сервера авторизации, которую мы можем использовать для получения токена.

# 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

Вся эта информация предоставляется через файл конфигурации MicroProfile, META-INF/microprofile-config.properties:

4.2. Запрос кода авторизации

Поток получения кода авторизации начинается с клиента, перенаправляя браузер на конечную точку авторизации сервера авторизации.

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

    @Inject
    private Config config;

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

Как правило, это происходит, когда пользователь пытается получить доступ к API защищенного ресурса без авторизации или путем явного вызова пути client/authorize:

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

В методе doGet() мы начинаем с создания и сохранения значение состояния безопасности:

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;

Затем мы добавим эти фрагменты информации в качестве параметров запроса к конечной точке авторизации сервера авторизации:

response.sendRedirect(authorizationLocation);

~~ ~ И, наконец, мы перенаправим браузер на этот URL-адрес:

После обработки запроса конечная точка авторизации сервера авторизации сгенерирует и добавит код, в дополнение к полученному параметру состояния, к redirect_uri и будет перенаправить обратно браузер http://localhost:9081/callback?code=A123\u0026state=Y.

4.3. Запрос токена доступа

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;
}

Сервлет обратного вызова клиента, /callback, начинается с проверки полученного состояния:

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);

Далее мы будем использовать ранее полученный код для запроса токена доступа через конечную точку токена сервера авторизации:

Как мы видим, для этого вызова нет взаимодействия с браузером, и запрос выполняется напрямую с использованием клиентского API JAX-RS в виде HTTP POST.

Поскольку конечная точка токена требует аутентификации клиента, мы включили учетные данные клиента client_id и client_secret в заголовок авторизации.

Клиент может использовать этот токен доступа для вызова API-интерфейсов сервера ресурсов, которые рассматриваются в следующем подразделе.

4.4. Защищенный доступ к ресурсам

На данный момент у нас есть действительный токен доступа, и мы можем вызывать API-интерфейсы /read и /write сервера ресурсов.

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

Для этого мы должны предоставить заголовок авторизации. Используя клиентский API JAX-RS, это просто делается с помощью метода Invocation.Builder header():

5. Сервер ресурсов OAuth 2.0

В этом разделе мы создадим защищенное веб-приложение. на основе JAX-RS, MicroProfile JWT и MicroProfile Config. MicroProfile JWT обеспечивает проверку полученного JWT и сопоставление областей JWT с ролями Jakarta EE.

5.1. Зависимости Maven

<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>

В дополнение к зависимости веб-API Java EE нам также потребуются API-интерфейсы MicroProfile Config и MicroProfile JWT:

5.2. Механизм аутентификации JWT

MicroProfile JWT обеспечивает реализацию механизма аутентификации токена-носителя. Это обеспечивает обработку JWT, присутствующего в заголовке авторизации, делает доступным субъект безопасности Jakarta EE в виде JsonWebToken, который содержит утверждения JWT, и сопоставляет области с ролями Jakarta EE. Дополнительные сведения см. в Jakarta EE Security API.

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

Чтобы включить механизм аутентификации JWT на сервере, нам нужно добавить аннотацию LoginConfig в приложение JAX-RS:

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

«

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

«Кроме того, MicroProfile JWT требуется открытый ключ RSA для проверки подписи JWT. Мы можем обеспечить это либо самоанализом, либо, для простоты, вручную скопировав ключ с сервера авторизации. В любом случае нам нужно предоставить расположение открытого ключа:

Наконец, JWT MicroProfile должен проверить утверждение iss входящего JWT, которое должно присутствовать и соответствовать значению свойства MicroProfile Config. :

Обычно это расположение Сервера авторизации.

5.3. Защищенные конечные точки

@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();
    }
}

В демонстрационных целях мы добавим ресурсный API с двумя конечными точками. Одна из них — это конечная точка чтения, доступная пользователям с областью ресурсов resource.read, а другая — конечная точка записи для пользователей с областью ресурсов resource.write.

Ограничение областей действия осуществляется через аннотацию @RolesAllowed:

mvn package liberty:run-server

6. Запуск всех серверов

# Authorization Server
http://localhost:9080/

# Client
http://localhost:9180/

# Resource Server
http://localhost:9280/

Для запуска одного сервера нам достаточно вызвать команду Maven в соответствующем каталоге: ~~ ~

Сервер авторизации, клиент и сервер ресурсов будут запущены и доступны соответственно в следующих местах:

Итак, мы можем получить доступ к домашней странице клиента, а затем нажимаем «Получить токен доступа». €, чтобы начать процесс авторизации. После получения токена доступа мы можем получить доступ к API чтения и записи сервера ресурсов.

В зависимости от предоставленных областей сервер ресурсов ответит либо успешным сообщением, либо мы получим запрещенный статус HTTP 403.

7. Заключение

В этой статье мы представили реализацию сервера авторизации OAuth 2.0, который можно использовать с любым совместимым клиентом OAuth 2.0 и сервером ресурсов.

Чтобы объяснить общую структуру, мы также предоставили реализацию для клиента и сервера ресурсов. Для реализации всех этих компонентов мы использовали API-интерфейсы Jakarta EE 8, особенно CDI, Servlet, JAX RS, Jakarta EE Security. Кроме того, мы использовали псевдо-Jakarta EE API MicroProfile: MicroProfile Config и MicroProfile JWT.