Implementando a estrutura de autorização do OAuth 2.0 usando Java EE
1. Visão geral
Neste tutorial, vamos fornecer uma implementação paraOAuth 2.0 Authorization Framework usando Java EE e MicroProfile. Mais importante ainda, vamos implementar a interação deOAuth 2.0 roles por meio deAuthorization Code grant type. A motivação por trás desta escrita é dar suporte para projetos que são implementados usando Java EE, pois isso ainda não fornece suporte para OAuth.
Para a função mais importante, o Authorization Server,we’re going to implement the Authorization Endpoint, the Token Endpoint and additionally, the JWK Key Endpoint, que é útil para o Resource Server recuperar a chave pública.
Como queremos que a implementação seja simples e fácil para uma configuração rápida, usaremos uma loja pré-registrada de clientes e usuários e, obviamente, uma loja JWT para tokens de acesso.
2. Visão geral do OAuth 2.0
Nesta seção, daremos uma breve visão geral dos papéis do OAuth 2.0 e do fluxo de concessão do código de autorização.
2.1. Funções
A estrutura do OAuth 2.0 implica a colaboração entre as quatro seguintes funções:
-
Resource Owner: normalmente, este é o usuário final - é a entidade que possui alguns recursos que valem a pena proteger
-
Resource Server: um serviço que protege os dados do proprietário do recurso, geralmente publicando-os por meio de uma API REST
-
Client: um aplicativo que usa os dados do proprietário do recurso
-
Authorization Server: um aplicativo que concede permissão - ou autoridade - a clientes na forma de tokens expirados
2.2. Tipos de concessão de autorização
Agrant type é como um cliente obtém permissão para usar os dados do proprietário do recurso, em última análise, na forma de um token de acesso.
Naturalmente, diferentes tipos de clientesprefer different types of grants:
-
Authorization Code:Preferred most often – quer sejaa web application, a native application, or a single-page application, embora os aplicativos nativos e de página única exijam proteção adicional chamada PKCE
-
Refresh Token: Uma concessão de renovação especial,suitable for web applications para renovar seu token existente
-
Client Credentials: preferido paraservice-to-service communication, digamos quando o proprietário do recurso não é um usuário final
-
Resource OwnerPassword: preferido para ofirst-party authentication of native applications, say quando o aplicativo móvel precisa de sua própria página de login
Além disso, o cliente pode usar o tipo de concessãoimplicit. No entanto, geralmente é mais seguro usar a concessão de código de autorização com o PKCE.
2.3. Fluxo de concessão do código de autorização
Já que o fluxo de concessão do código de autorização é o mais comum, vamos também revisar como isso funciona, ethat’s actually what we’ll build in this tutorial.
Um aplicativo - um cliente -requests permission by redirecting to the authorization server’s /authorize endpoint. Para este endpoint, o aplicativo fornece um endpointcallback.
O servidor de autorização geralmente solicita permissão ao usuário final - o proprietário do recurso. Se o usuário final conceder permissão, entãothe authorization server redirects back to the callback comcode.
A aplicação recebe este código e entãomakes an authenticated call to the authorization server’s /token endpoint. Por “autenticado”, significa que a aplicação prova quem é no âmbito desta chamada. Se tudo aparecer em ordem, o servidor de autorização responde com o token.
Com o token em mãos,the application makes its request to the API - o servidor de recursos - e essa API verificarão o token. Ele pode solicitar que o servidor de autorização verifique o token usando seu ponto de extremidade/introspect. Ou, se o token for independente, o servidor de recursos pode otimizar emlocally verifying the token’s signature, as is the case with JWT.
2.4. O que o Java EE suporta?
Ainda não muito. Neste tutorial, vamos construir a maioria das coisas do zero.
3. Servidor de autorização do OAuth 2.0
Nesta implementação, vamos nos concentrar emthe most commonly used grant type: Código de Autorização.
3.1. Registro de Cliente e Usuário
Um servidor de autorização, é claro, precisaria conhecer os clientes e usuários para poder autorizar seus pedidos. E é comum que um servidor de autorização tenha uma IU para isso.
Para simplificar, porém, usaremos um cliente pré-configurado:
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;
// ...
}
E um usuário pré-configurado:
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;
// ...
}
Observe que, para fins deste tutorial, usamos senhas em texto simples,but in a production environment, they should be hashed.
Para o resto deste tutorial, mostraremos comoappuser – o proprietário do recurso - pode conceder acesso awebappclient - o aplicativo - implementando o Código de Autorização.
3.2. Ponto de extremidade de autorização
A principal função do endpoint de autorização é primeiroauthenticate the user and then ask for the permissions - ou escopos - que o aplicativo deseja.
Comoinstructed by the OAuth2 specs, este ponto de extremidade deve suportar o método HTTP GET, embora também possa suportar o método HTTP POST. Nesta implementação, ofereceremos suporte apenas ao método HTTP GET.
Primeiro,the authorization endpoint requires that the user be authenticated. A especificação não exige uma determinada maneira aqui, então vamos usar a autenticação de formulário doJava EE 8 Security API:
@FormAuthenticationMechanismDefinition(
loginToContinue = @LoginToContinue(loginPage = "/login.jsp", errorPage = "/login.jsp")
)
O usuário será redirecionado para/login.jsp para autenticação e então estará disponível comoCallerPrincipal por meio da API SecurityContext:
Principal principal = securityContext.getCallerPrincipal();
Podemos reuni-los usando o 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 params = uriInfo.getQueryParameters();
Principal principal = securityContext.getCallerPrincipal();
// ...
}
}
Neste ponto, o endpoint de autorização pode começar a processar a solicitação do aplicativo, que deve conterresponse_type and client_id parameters and – optionally, but recommended – the redirect_uri, scope, and state parameters.
Oclient_id deve ser um cliente válido, em nosso caso da tabela de banco de dadosclients.
Oredirect_uri, se especificado, também deve corresponder ao que encontramos na tabela de banco de dadosclients.
E, como estamos fazendo o Código de Autorização,response_type écode.
Como a autorização é um processo de várias etapas, podemos armazenar temporariamente esses valores na sessão:
request.getSession().setAttribute("ORIGINAL_PARAMS", params);
E, em seguida, prepare-se para perguntar ao usuário quais permissões o aplicativo pode usar, redirecionando para essa página:
String allowedScopes = checkUserScopes(user.getScopes(), requestedScope);
request.setAttribute("scopes", allowedScopes);
request.getRequestDispatcher("/authorize.jsp").forward(request, response);
3.3. Aprovação de escopos do usuário
Neste ponto, o navegador renderiza uma IU de autorização para o usuário ethe user makes a selection. Em seguida, o navegadorsubmits the user’s selection in an HTTP POST:
@POST
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
@Produces(MediaType.TEXT_HTML)
public Response doPost(@Context HttpServletRequest request, @Context HttpServletResponse response,
MultivaluedMap params) throws Exception {
MultivaluedMap originalParams =
(MultivaluedMap) request.getSession().getAttribute("ORIGINAL_PARAMS");
// ...
String approvalStatus = params.getFirst("approval_status"); // YES OR NO
// ... if YES
List approvedScopes = params.get("scope");
// ...
}
Em seguida, geramos um código temporário que se refere auser_id, client_id, andredirect_uri, que o aplicativo usará posteriormente quando atingir o ponto de extremidade do token.
Então, vamos criar uma Entidade JPAAuthorizationCode com um id gerado automaticamente:
@Entity
@Table(name ="authorization_code")
public class AuthorizationCode {
@Id
@GeneratedValue(strategy=GenerationType.AUTO)
@Column(name = "code")
private String code;
//...
}
E, em seguida, preencha:
AuthorizationCode authorizationCode = new AuthorizationCode();
authorizationCode.setClientId(clientId);
authorizationCode.setUserId(userId);
authorizationCode.setApprovedScopes(String.join(" ", authorizedScopes));
authorizationCode.setExpirationDate(LocalDateTime.now().plusMinutes(2));
authorizationCode.setRedirectUri(redirectUri);
Quando salvamos o bean, o atributo de código é preenchido automaticamente e, portanto, podemos obtê-lo e enviá-lo de volta ao cliente:
appDataRepository.save(authorizationCode);
String code = authorizationCode.getCode();
Observe queour authorization code will expire in two minutes - devemos ser o mais conservadores que pudermos com essa expiração. Pode ser curto, pois o cliente irá trocá-lo imediatamente por um token de acesso.
Em seguida, redirecionamos de volta pararedirect_uri, do aplicativo, fornecendo a ele o código, bem como qualquer parâmetrostate que o aplicativo especificou em sua solicitação/authorize:
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();
Observe novamente queredirectUri is whatever exists in the clients table, not the redirect_uri request parameter.
Portanto, nossa próxima etapa é que o cliente receba esse código e troque-o por um token de acesso usando o ponto de extremidade do token.
3.4. Ponto final do token
Ao contrário do endpoint de autorização, o endpoint de tokendoesn’t need a browser to communicate with the client, e vamos, portanto, implementá-lo como um endpoint JAX-RS:
@Path("token")
public class TokenEndpoint {
List supportedGrantTypes = Collections.singletonList("authorization_code");
@Inject
private AppDataRepository appDataRepository;
@Inject
Instance authorizationGrantTypeHandlers;
@POST
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
public Response token(MultivaluedMap params,
@HeaderParam(HttpHeaders.AUTHORIZATION) String authHeader) throws JOSEException {
//...
}
}
O terminal de token requer um POST, bem como a codificação dos parâmetros usando o tipo de mídiaapplication/x-www-form-urlencoded.
Como discutimos, apoiaremos apenas o tipo de concessãoauthorization code:
List supportedGrantTypes = Collections.singletonList("authorization_code");
Portanto, ogrant_type recebido como um parâmetro obrigatório deve ser suportado:
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();
}
Em seguida, verificamos a autenticação do cliente por meio da autenticação HTTP Basic. Ou seja, verificamosif the received client_id and client_secret, por meio do cabeçalhoAuthorization,matches a registered client:
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();
}
Finalmente, delegamos a produção deTokenResponse a um manipulador de tipo de concessão correspondente:
public interface AuthorizationGrantTypeHandler {
TokenResponse createAccessToken(String clientId, MultivaluedMap params) throws Exception;
}
Como estamos mais interessados no tipo de concessão do código de autorização, fornecemos uma implementação adequada como um bean CDI e o decoramos com a anotaçãoNamed:
@Named("authorization_code")
Em tempo de execução, e de acordo com o valorgrant_type recebido, a implementação correspondente é ativada através doCDI Instance mechanism:
String grantType = params.getFirst("grant_type");
//...
AuthorizationGrantTypeHandler authorizationGrantTypeHandler =
authorizationGrantTypeHandlers.select(NamedLiteral.of(grantType)).get();
Agora é hora de produzir a resposta de/token.
3.5. Chaves públicas e privadas da RSA
Antes de gerar o token, precisamos de uma chave privada RSA para assinar tokens.
Para este propósito, usaremos OpenSSL:
# PRIVATE KEY
openssl genpkey -algorithm RSA -out private-key.pem -pkeyopt rsa_keygen_bits:2048
Oprivate-key.pem é fornecido ao servidor por meio da propriedade MicroProfile ConfigsigningKey usando o arquivoMETA-INF/microprofile-config.properties:
signingkey=/META-INF/private-key.pem
O servidor pode ler a propriedade usando o objetoConfig injetado:
String signingkey = config.getValue("signingkey", String.class);
Da mesma forma, podemos gerar a chave pública correspondente:
# PUBLIC KEY
openssl rsa -pubout -in private-key.pem -out public-key.pem
E use o MicroProfile ConfigverificationKey para lê-lo:
verificationkey=/META-INF/public-key.pem
O servidor deve disponibilizá-lo para o servidor de recursos porthe purpose of verification. Isso é feitothrough a JWK endpoint.
Nimbus JOSE+JWT é uma biblioteca que pode ser de grande ajuda aqui. Vamos primeiro adicionarthe nimbus-jose-jwt dependency:
com.nimbusds
nimbus-jose-jwt
7.7
E agora, podemos aproveitar o suporte JWK da Nimbus para simplificar nosso endpoint:
@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();
}
//...
}
}
Usamos o formatoparameter para alternar entre os formatos PEM e JWK. O MicroProfile JWT que usaremos para implementar o servidor de recursos oferece suporte a esses dois formatos.
3.6. Resposta do Terminal de Token
Agora é a hora de um determinadoAuthorizationGrantTypeHandler criar a resposta do token. Nesta implementação, ofereceremos suporte apenas aos Tokens JWT estruturados.
For creating a token in this format, we’ll again use the Nimbus JOSE+JWT library, mas também hánumerous other JWT libraries.
Portanto, para criar um JWT assinado,we first have to construct the JWT header:
JWSHeader jwsHeader = new JWSHeader.Builder(JWSAlgorithm.RS256).type(JOSEObjectType.JWT).build();
Then, we build the payload que é umSet de declarações padronizadas e personalizadas:
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);
Além das declarações JWT padrão, adicionamos mais duas declarações -upnegroups - conforme são necessárias para o MicroProfile JWT. Oupn será mapeado para Java EE SecurityCallerPrincipal egroups será mapeado para Java EERoles.
Agora que temos o cabeçalho e a carga útil,we need to sign the access token with an RSA private key. A chave pública RSA correspondente será exposta através do terminal JWK ou disponibilizada por outros meios, para que o servidor de recursos possa usá-la para verificar o token de acesso.
Como fornecemos a chave privada como um formato PEM, devemos recuperá-la e transformá-la em umRSAPrivateKey:
SignedJWT signedJWT = new SignedJWT(jwsHeader, jwtClaims);
//...
String signingkey = config.getValue("signingkey", String.class);
String pemEncodedRSAPrivateKey = PEMKeyUtils.readKeyAsString(signingkey);
RSAKey rsaKey = (RSAKey) JWK.parseFromPEMEncodedObjects(pemEncodedRSAPrivateKey);
A seguir,we sign and serialize the JWT:
signedJWT.sign(new RSASSASigner(rsaKey.toRSAPrivateKey()));
String accessToken = signedJWT.serialize();
E finalmentewe construct a token response:
return Json.createObjectBuilder()
.add("token_type", "Bearer")
.add("access_token", accessToken)
.add("expires_in", expiresInMin * 60)
.add("scope", authorizationCode.getApprovedScopes())
.build();
que é, graças ao JSON-P, serializado no formato JSON e enviado ao cliente:
{
"access_token": "acb6803a48114d9fb4761e403c17f812",
"token_type": "Bearer",
"expires_in": 1800,
"scope": "resource.read resource.write"
}
4. Cliente OAuth 2.0
Nesta seção, estaremosbuilding a web-based OAuth 2.0 Client usando as APIs Servlet, MicroProfile Config e JAX RS Client.
Mais precisamente, implementaremos dois servlets principais: um para solicitar o endpoint de autorização do servidor de autorização e obter um código usando o tipo de concessão de código de autorização e outro servlet para usar o código recebido e solicitar um token de acesso do endpoint de token do servidor de autorização .
Além disso, implementaremos mais dois servlets: um para obter um novo token de acesso usando o tipo de concessão do token de atualização e outro para acessar as APIs do servidor de recursos.
4.1. Detalhes do cliente OAuth 2.0
Como o cliente já está registrado no servidor de autorização, primeiro precisamos fornecer as informações de registro do cliente:
-
client_id: Identificador de cliente e geralmente é emitido pelo servidor de autorização durante o processo de registro.
-
client_secret: Segredo do cliente.
-
redirect_uri: Local onde receber o código de autorização.
-
scope: Permissões solicitadas pelo cliente.
Além disso, o cliente deve saber a autorização do servidor de autorização e os terminais de token:
-
authorization_uri: Localização do endpoint de autorização do servidor de autorização que podemos usar para obter um código.
-
token_uri: Localização do terminal do token do servidor de autorização que podemos usar para obter um token.
Todas essas informações são fornecidas através do arquivo de configuração do MicroProfile,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. Solicitação de código de autorização
O fluxo de obtenção de um código de autorização começa com o cliente, redirecionando o navegador para o endpoint de autorização do servidor de autorização.
Normalmente, isso acontece quando o usuário tenta acessar uma API de recurso protegido sem autorização ou explicitamente invocando o caminho/authorize do cliente:
@WebServlet(urlPatterns = "/authorize")
public class AuthorizationCodeServlet extends HttpServlet {
@Inject
private Config config;
@Override
protected void doGet(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
//...
}
}
No métododoGet(), começamos gerando e armazenando um valor de estado de segurança:
String state = UUID.randomUUID().toString();
request.getSession().setAttribute("CLIENT_LOCAL_STATE", state);
Em seguida, recuperamos as informações de configuração do cliente:
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);
Em seguida, anexaremos essas informações como parâmetros de consulta ao endpoint de autorização do servidor de autorização:
String authorizationLocation = authorizationUri + "?response_type=code"
+ "&client_id=" + clientId
+ "&redirect_uri=" + redirectUri
+ "&scope=" + scope
+ "&state=" + state;
E, por fim, redirecionaremos o navegador para este URL:
response.sendRedirect(authorizationLocation);
Após o processamento da solicitação,the authorization server’s authorization endpoint will generate and append a code, além do parâmetro de estado recebido, para oredirect_urie redirecionará de volta o navegadorhttp://localhost:9081/callback?code=A123&state=Y.
4.3. Solicitação de token de acesso
O servlet de retorno de chamada do cliente,/callback, começa validando ostate: recebido
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;
}
Em seguida,we’ll use the code we previously received to request an access token por meio do terminal de token do servidor de autorização:
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);
Como podemos ver, não há interação do navegador para esta chamada e a solicitação é feita diretamente usando a API do cliente JAX-RS como um HTTP POST.
Como o terminal de token requer a autenticação do cliente, incluímos as credenciais do clienteclient_ideclient_secret no cabeçalhoAuthorization.
O cliente pode usar esse token de acesso para chamar as APIs do servidor de recursos, que são o assunto da próxima subseção.
4.4. Acesso a Recursos Protegidos
Neste ponto, temos um token de acesso válido e podemos chamar as APIs /reade /write do servidor de recursos.
Para fazer isso,we have to provide the Authorization header. Usando a API do cliente JAX-RS, isso é feito simplesmente por meio do métodoInvocation.Builder header():
resourceWebTarget = webTarget.path("resource/read");
Invocation.Builder invocationBuilder = resourceWebTarget.request();
response = invocationBuilder
.header("authorization", tokenResponse.getString("access_token"))
.get(String.class);
5. Servidor de recursos do OAuth 2.0
Nesta seção, estaremos construindo um aplicativo da web seguro baseado em JAX-RS, MicroProfile JWT e MicroProfile Config. The MicroProfile JWT takes care of validating the received JWT and mapping the JWT scopes to Java EE roles.
5.1. Dependências do Maven
Além da dependênciaJava EE Web API, precisamos também das APIsMicroProfile ConfigeMicroProfile JWT:
javax
javaee-web-api
8.0
provided
org.eclipse.microprofile.config
microprofile-config-api
1.3
org.eclipse.microprofile.jwt
microprofile-jwt-auth-api
1.1
5.2. Mecanismo de autenticação JWT
O MicroProfile JWT fornece uma implementação do mecanismo de autenticação de token do portador. Isso cuida do processamento do JWT presente no cabeçalhoAuthorization, disponibiliza um Java EE Security Principal como umJsonWebToken que contém as declarações JWT e mapeia os escopos para funções Java EE. Dê uma olhada emJAVA EE Security API para mais informações.
Para ativar oJWT authentication mechanism in the server,, precisamosadd the LoginConfig annotation no aplicativo JAX-RS:
@ApplicationPath("/api")
@DeclareRoles({"resource.read", "resource.write"})
@LoginConfig(authMethod = "MP-JWT")
public class OAuth2ResourceServerApplication extends Application {
}
Além disso,MicroProfile JWT needs the RSA public key in order to verify the JWT signature. Podemos fornecer isso por introspecção ou, por simplicidade, copiando manualmente a chave do servidor de autorização. Em ambos os casos, precisamos fornecer o local da chave pública:
mp.jwt.verify.publickey.location=/META-INF/public-key.pem
Finalmente, o MicroProfile JWT precisa verificar a declaraçãoiss do JWT de entrada, que deve estar presente e corresponder ao valor da propriedade MicroProfile Config:
mp.jwt.verify.issuer=http://127.0.0.1:9080
Normalmente, esse é o local do Servidor de Autorização.
5.3. Os pontos de extremidade protegidos
Para fins de demonstração, adicionaremos uma API de recursos com dois terminais. Um é um endpointread que pode ser acessado por usuários com escoporesource.read e outro endpointwrite para usuários com escoporesource.write.
A restrição nos escopos é feita através da anotação@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. Executando todos os servidores
Para executar um servidor, basta chamar o comando Maven no diretório correspondente:
mvn package liberty:run-server
O servidor de autorização, o cliente e o servidor de recursos estarão em execução e disponíveis, respectivamente, nos seguintes locais:
# Authorization Server
http://localhost:9080/
# Client
http://localhost:9180/
# Resource Server
http://localhost:9280/
Assim, podemos acessar a página inicial do cliente e clicar em "Obter acesso token" para iniciar o fluxo de autorização. Depois de receber o token de acesso, podemos acessar as APIsreadewrite do servidor de recursos.
Dependendo dos escopos concedidos, o servidor de recursos responderá por uma mensagem de sucesso ou obteremos um status HTTP 403 proibido.
7. Conclusão
Neste artigo, fornecemos uma implementação de um servidor de autorização OAuth 2.0 que pode ser usado com qualquer cliente e servidor de recursos OAuth 2.0 compatível.
Para explicar a estrutura geral, também fornecemos uma implementação para o cliente e o servidor de recursos. Para implementar todos esses componentes, usamos APIs Java EE 8, especialmente CDI, Servlet, JAX RS, Segurança JAVA EE. Além disso, usamos as APIs pseudo-Java EE do MicroProfile: MicroProfile Config e MicroProfile JWT.
O código-fonte completo dos exemplos está disponívelover on GitHub. Observe que o código inclui um exemplo dos tipos de concessão de código de autorização e de token de atualização.