Skip to content

Commit

Permalink
Merge pull request #11446 from sberyozkin/security_builder_and_doc_im…
Browse files Browse the repository at this point in the history
…provements

Add a new builder identity copy method, reverse links to all security docs, client cert example
  • Loading branch information
gsmet authored Aug 19, 2020
2 parents e3fa4cf + 05b1685 commit c0d5f00
Show file tree
Hide file tree
Showing 18 changed files with 159 additions and 16 deletions.
4 changes: 4 additions & 0 deletions docs/src/main/asciidoc/security-authorization.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -211,3 +211,7 @@ public class SubjectExposingResource {
<3> The `/subject/unsecured` endpoint allows for unauthenticated access by specifying the `@PermitAll` annotation.
<4> This call to obtain the user principal will return null if the caller is unauthenticated, non-null if the caller is authenticated.
<5> The `/subject/denied` endpoint disallows any access regardless of whether the call is authenticated by specifying the `@DenyAll` annotation.

== References

* link:security[Quarkus Security]
11 changes: 11 additions & 0 deletions docs/src/main/asciidoc/security-built-in-authentication.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -87,10 +87,18 @@ You should also be able to get the certificate as follows:
.Obtaining the certificate
[source,java]
----
import java.security.cert.X509Certificate;
import io.quarkus.security.credential.CertificateCredential;
CertificateCredential credential = identity.getCredential(CertificateCredential.class);
X509Certificate certificate = credential.getCertificate();
----

=== Authorization

The information from the client certificate can be used to enhance Quarkus `SecurityIdentity`. For example, one can add new roles after checking a client certificate subject name, etc.
Please see the link:security-customization#security-identity-customization[SecurityIdentity Customization] section for more information about customizing Quarkus `SecurityIdentity`.

[[proactive-authentication]]
== Proactive Authentication

Expand All @@ -100,3 +108,6 @@ credential then that request will always be authenticated (even if the target pa
This means that requests with an invalid credential will always be rejected, even for public pages. You can change
this behavior and only authenticate when required by setting `quarkus.http.auth.proactive=false`.

== References

* link:security[Quarkus Security]
72 changes: 65 additions & 7 deletions docs/src/main/asciidoc/security-customization.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ public class CustomAwareJWTAuthMechanism implements HttpAuthenticationMechanism
}
----

[[security-identity-customization]]
== Security Identity Customization

Internally, the identity providers create and update an instance of the `io.quarkus.security.identity.SecurityIdentity` class which holds the principal, roles, credentials which were used to authenticate the client (user) and other security attributes. An easy option to customize `SecurityIdentity` is to register a custom `SecurityIdentityAugmentor`. For example, the augmentor below adds an addition role:
Expand All @@ -73,19 +74,18 @@ public class RolesAugmentor implements SecurityIdentityAugmentor {
@Override
public Uni<SecurityIdentity> augment(SecurityIdentity identity, AuthenticationRequestContext context) {
return context.runBlocking(build(identity));
return Uni.createFrom().item(build(identity));
// Do 'return context.runBlocking(build(identity));'
// if a blocking call is required to customize the identity
}
private Supplier<SecurityIdentity> build(SecurityIdentity identity) {
if(identity.isAnonymous()) {
return () -> identity;
} else {
// create a new builder and copy principal, attributes, credentials and roles from the original
QuarkusSecurityIdentity.Builder builder = QuarkusSecurityIdentity.builder()
.setPrincipal(identity.getPrincipal())
.addAttributes(identity.getAttributes())
.addCredentials(identity.getCredentials())
.addRoles(identity.getRoles());
// create a new builder and copy principal, attributes, credentials and roles from the original identity
QuarkusSecurityIdentity.Builder builder = QuarkusSecurityIdentity.builder(identity);
// add custom role source here
builder.addRole("dummy");
Expand All @@ -95,6 +95,61 @@ public class RolesAugmentor implements SecurityIdentityAugmentor {
}
----

Here is another example showing how to use the client certificate available in the current link:security-built-in-authentication#mutual-tls[Mutual TLS] request to add more roles:

[source,java]
----
import java.security.cert.X509Certificate;
import io.quarkus.security.credential.CertificateCredential;
import io.quarkus.security.identity.AuthenticationRequestContext;
import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.security.identity.SecurityIdentityAugmentor;
import io.quarkus.security.runtime.QuarkusSecurityIdentity;
import io.smallrye.mutiny.Uni;
import javax.enterprise.context.ApplicationScoped;
import java.util.function.Supplier;
import java.util.Set;
@ApplicationScoped
public class RolesAugmentor implements SecurityIdentityAugmentor {
@Override
public int priority() {
return 0;
}
@Override
public Uni<SecurityIdentity> augment(SecurityIdentity identity, AuthenticationRequestContext context) {
return Uni.createFrom().item(build(identity));
}
private Supplier<SecurityIdentity> build(SecurityIdentity identity) {
// create a new builder and copy principal, attributes, credentials and roles from the original identity
QuarkusSecurityIdentity.Builder builder = QuarkusSecurityIdentity.builder(identity);
CertificateCredential certificate = identity.getCredential(CertificateCredential.class);
if (certificate != null) {
builder.addRoles(extractRoles(certificate.getCertificate()));
}
return builder::build;
}
private Set<String> extractRoles(X509Certificate certificate) {
String name = certificate.getSubjectX500Principal().getName();
switch (name) {
case "CN=client":
return Collections.singleton("user");
case "CN=guest-client":
return Collections.singleton("guest");
default:
return Collections.emptySet();
}
}
}
----

== Custom JAX-RS SecurityContext

If you use JAX-RS `ContainerRequestFilter` to set a custom JAX-RS `SecurityContext` then make sure `ContainerRequestFilter` runs in the JAX-RS pre-match phase by adding a `@PreMatching` annotation to it for this custom security context to be linked with Quarkus `SecurityIdentity`, for example:
Expand Down Expand Up @@ -199,3 +254,6 @@ are using an executor that is capable of propagating the identity (e.g. no `Comp
to make sure that Quarkus can propagate it. For more information see the
link:context-propagation[Context Propagation Guide].

== References

* link:security[Quarkus Security]
5 changes: 2 additions & 3 deletions docs/src/main/asciidoc/security-jdbc.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,6 @@ quarkus.security.jdbc.principal-query.roles.attribute-mappings.0.to=groups

include::{generated-dir}/config/quarkus-elytron-security-jdbc.adoc[opts=optional, leveloffset=+1]

== Future Work
== References

* Propose more password mappers.
* Provide an opinionated configuration.
* link:security[Quarkus Security]
4 changes: 4 additions & 0 deletions docs/src/main/asciidoc/security-jpa.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -376,3 +376,7 @@ they're all stored in the hashed value.

WARN: you can also store passwords in clear text with `@Password(PasswordType.CLEAR)` but we strongly recommend against
it in production.

== References

* link:security[Quarkus Security]
1 change: 1 addition & 0 deletions docs/src/main/asciidoc/security-jwt.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -866,3 +866,4 @@ Smallrye JWT supports the following properties which can be used to customize th
* link:https://tools.ietf.org/html/rfc7515[JSON Web Signature]
* link:https://tools.ietf.org/html/rfc7516[JSON Web Encryption]
* link:https://tools.ietf.org/html/rfc7518[JSON Web Algorithms]
* link:security[Quarkus Security]
Original file line number Diff line number Diff line change
Expand Up @@ -359,3 +359,4 @@ include::{generated-dir}/config/quarkus-keycloak-keycloak-policy-enforcer-config
* https://www.keycloak.org/docs/latest/authorization_services/index.html[Keycloak Authorization Services Documentation]
* https://openid.net/connect/[OpenID Connect]
* https://tools.ietf.org/html/rfc7519[JSON Web Token]
* link:security[Quarkus Security]
5 changes: 5 additions & 0 deletions docs/src/main/asciidoc/security-ldap.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -242,3 +242,8 @@ user%
== Configuration Reference

include::{generated-dir}/config/quarkus-elytron-security-ldap.adoc[opts=optional, leveloffset=+1]

== References

* https://en.wikipedia.org/wiki/Lightweight_Directory_Access_Protocol[LDAP]
* link:security[Quarkus Security]
5 changes: 5 additions & 0 deletions docs/src/main/asciidoc/security-oauth2.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -465,3 +465,8 @@ class TokenSecuredResourceTest {
====
`@QuarkusTestResource` applies to all tests, not just `TokenSecuredResourceTest`.
====

== References

* https://tools.ietf.org/html/rfc6749[OAuth2]
* link:security[Quarkus Security]
Original file line number Diff line number Diff line change
Expand Up @@ -360,3 +360,4 @@ include::{generated-dir}/config/quarkus-oidc.adoc[opts=optional]
* https://openid.net/connect/[OpenID Connect]
* https://tools.ietf.org/html/rfc7519[JSON Web Token]
* https://developers.google.com/identity/protocols/OpenIDConnect[Google OpenID Connect]
* link:security[Quarkus Security]
Original file line number Diff line number Diff line change
Expand Up @@ -287,10 +287,9 @@ If UserInfo is the source of the roles then set `quarkus.oidc.user-info-required

Please check if implementing SPAs the way it is suggested in the link:security-openid-connect#single-page-applications[Single Page Applications for Service Applications] section can meet your requirements.

If you do prefer to use SPA and `XMLHttpRequest`(XHR) with Quarkus `web-app` applications then please be aware that OpenId Connect Providers may not support CORS for Authorization endpoints where the users
are authenticated after a redirect from Quarkus which will lead to the authentication failures if the Quarkus `web-app` application and OpenId Connect Provider are hosted on the different HTTP domains/ports.
If you prefer to use SPA and `XMLHttpRequest`(XHR) with Quarkus applications, please be aware that OpenID Connect Providers may not support CORS for Authorization endpoints where the users are authenticated after a redirect from Quarkus. This will lead to authentication failures if the Quarkus application and the OpenID Connect Provider are hosted on the different HTTP domains/ports.

In such cases one needs to set the `quarkus.oidc.authentication.xhr-auto-redirect` property to `false` which will instruct Quarkus to return a `499` status code and `WWW-Authenticate` header with the `OIDC` value and the browser script needs to be updated to set "X-Requested-With" header with the `XMLHttpRequest` value and reload the last requested page in case of `499`, for example:
In such cases, set the `quarkus.oidc.authentication.xhr-auto-redirect` property to `false` which will instruct Quarkus to return a `499` status code and `WWW-Authenticate` header with the `OIDC` value. The browser script also needs to be updated to set `X-Requested-With` header with the `XMLHttpRequest` value and reload the last requested page in case of `499`, for example:

[source,javascript]
----
Expand All @@ -301,7 +300,7 @@ Future<void> callQuarkusService() async {
.get("https://localhost:443/serviceCall")
.then((response) {
if (response.statusCode == 499) {
window.location.assign(https://localhost.com:443/serviceCall);
window.location.assign("https://localhost.com:443/serviceCall");
}
});
}
Expand All @@ -317,3 +316,4 @@ include::{generated-dir}/config/quarkus-oidc.adoc[opts=optional]
* https://www.keycloak.org/documentation.html[Keycloak Documentation]
* https://openid.net/connect/[OpenID Connect]
* https://tools.ietf.org/html/rfc7519[JSON Web Token]
* link:security[Quarkus Security]
3 changes: 2 additions & 1 deletion docs/src/main/asciidoc/security-openid-connect.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,7 @@ If the token is opaque (binary) then a `scope` property from the remote token in

Additionally a custom `SecurityIdentityAugmentor` can also be used to add the roles as documented link:security#security-identity-customization[here].

[[oidc-single-page-applications]]
[[single-page-applications]]
== Single Page Applications

Single Page Application (SPA) typically uses `XMLHttpRequest`(XHR) and the Java Script utility code provided by the OpenId Connect provider to acquire a bearer token and use it
Expand Down Expand Up @@ -397,3 +397,4 @@ For example, here is how you can use `keycloak.js` to authenticate the users and
* https://www.keycloak.org/documentation.html[Keycloak Documentation]
* https://openid.net/connect/[OpenID Connect]
* https://tools.ietf.org/html/rfc7519[JSON Web Token]
* link:security[Quarkus Security]
4 changes: 4 additions & 0 deletions docs/src/main/asciidoc/security-properties.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -134,3 +134,7 @@ quarkus.security.users.embedded.roles.noadmin=user
----
<1> User `scott` has roles `Admin`, `admin`, `Tester`, and `user`
<2> User `stuart` has roles `admin` and `user`

== References

* link:security[Quarkus Security]
3 changes: 3 additions & 0 deletions docs/src/main/asciidoc/security-testing.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -97,3 +97,6 @@ for example by setting `quarkus.http.auth.basic=true` or `%test.quarkus.http.aut
You can also use Wiremock to mock the authorization OAuth2 and OIDC services:
See link:security-oauth#integration-testing[OAuth2 Integration testing] for more details.

== References

* link:security[Quarkus Security]
7 changes: 6 additions & 1 deletion docs/src/main/asciidoc/vault.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -512,4 +512,9 @@ credentials), which goes a long way towards creating secured applications.
[[configuration-reference]]
== Configuration Reference

include::{generated-dir}/config/quarkus-vault.adoc[opts=optional, leveloffset=+1]
include::{generated-dir}/config/quarkus-vault.adoc[opts=optional, leveloffset=+1]

== References

* https://www.vaultproject.io/[HashiCorp Vault]
* link:security[Quarkus Security]
5 changes: 5 additions & 0 deletions extensions/security/runtime/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@
<groupId>jakarta.enterprise</groupId>
<artifactId>jakarta.enterprise.cdi-api</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5-internal</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,14 @@ public static Builder builder() {
return new Builder();
}

public static Builder builder(SecurityIdentity identity) {
return new Builder()
.addAttributes(identity.getAttributes())
.addCredentials(identity.getCredentials())
.addRoles(identity.getRoles())
.setPrincipal(identity.getPrincipal());
}

public static class Builder {

Principal principal;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package io.quarkus.security.runtime;

import static org.junit.jupiter.api.Assertions.assertEquals;

import org.junit.jupiter.api.Test;

import io.quarkus.security.credential.PasswordCredential;
import io.quarkus.security.identity.SecurityIdentity;

public class QuarkusSecurityIdentityTest {

@Test
public void testCopyIdentity() throws Exception {
SecurityIdentity identity1 = QuarkusSecurityIdentity.builder()
.setPrincipal(new QuarkusPrincipal("alice"))
.addRole("admin")
.addCredential(new PasswordCredential("password".toCharArray()))
.addAttribute("key", "value")
.build();

SecurityIdentity identity2 = QuarkusSecurityIdentity.builder(identity1).build();

assertEquals(identity1.getAttributes(), identity2.getAttributes());
assertEquals(identity1.getPrincipal(), identity2.getPrincipal());
assertEquals(identity1.getCredentials(), identity2.getCredentials());
assertEquals(identity1.getRoles(), identity2.getRoles());
}
}

0 comments on commit c0d5f00

Please sign in to comment.