diff --git a/docs/modules/ROOT/pages/includes/quarkus-google-cloud-firebase-admin.adoc b/docs/modules/ROOT/pages/includes/quarkus-google-cloud-firebase-admin.adoc index 6d062718..05adefd5 100644 --- a/docs/modules/ROOT/pages/includes/quarkus-google-cloud-firebase-admin.adoc +++ b/docs/modules/ROOT/pages/includes/quarkus-google-cloud-firebase-admin.adoc @@ -61,5 +61,22 @@ endif::add-copy-button-to-env-var[] |boolean |`true` +a| [[quarkus-google-cloud-firebase-admin_quarkus-google-cloud-firebase-auth-roles-claim]] [.property-path]##link:#quarkus-google-cloud-firebase-admin_quarkus-google-cloud-firebase-auth-roles-claim[`quarkus.google.cloud.firebase.auth.roles-claim`]## + +[.description] +-- +When set, the values in this claim in the Firebase JWT will be mapped to the roles in the Quarkus `io.quarkus.security.identity.SecurityIdentity`. This claim can either be a set of roles (i.e. an array in the JWT) or a single value. + + +ifdef::add-copy-button-to-env-var[] +Environment variable: env_var_with_copy_button:+++QUARKUS_GOOGLE_CLOUD_FIREBASE_AUTH_ROLES_CLAIM+++[] +endif::add-copy-button-to-env-var[] +ifndef::add-copy-button-to-env-var[] +Environment variable: `+++QUARKUS_GOOGLE_CLOUD_FIREBASE_AUTH_ROLES_CLAIM+++` +endif::add-copy-button-to-env-var[] +-- +|string +| + |=== diff --git a/docs/modules/ROOT/pages/includes/quarkus-google-cloud-firebase-admin_quarkus.google.adoc b/docs/modules/ROOT/pages/includes/quarkus-google-cloud-firebase-admin_quarkus.google.adoc index 6d062718..05adefd5 100644 --- a/docs/modules/ROOT/pages/includes/quarkus-google-cloud-firebase-admin_quarkus.google.adoc +++ b/docs/modules/ROOT/pages/includes/quarkus-google-cloud-firebase-admin_quarkus.google.adoc @@ -61,5 +61,22 @@ endif::add-copy-button-to-env-var[] |boolean |`true` +a| [[quarkus-google-cloud-firebase-admin_quarkus-google-cloud-firebase-auth-roles-claim]] [.property-path]##link:#quarkus-google-cloud-firebase-admin_quarkus-google-cloud-firebase-auth-roles-claim[`quarkus.google.cloud.firebase.auth.roles-claim`]## + +[.description] +-- +When set, the values in this claim in the Firebase JWT will be mapped to the roles in the Quarkus `io.quarkus.security.identity.SecurityIdentity`. This claim can either be a set of roles (i.e. an array in the JWT) or a single value. + + +ifdef::add-copy-button-to-env-var[] +Environment variable: env_var_with_copy_button:+++QUARKUS_GOOGLE_CLOUD_FIREBASE_AUTH_ROLES_CLAIM+++[] +endif::add-copy-button-to-env-var[] +ifndef::add-copy-button-to-env-var[] +Environment variable: `+++QUARKUS_GOOGLE_CLOUD_FIREBASE_AUTH_ROLES_CLAIM+++` +endif::add-copy-button-to-env-var[] +-- +|string +| + |=== diff --git a/firebase-admin/runtime/src/main/java/io/quarkiverse/googlecloudservices/firebase/admin/runtime/FirebaseAuthConfig.java b/firebase-admin/runtime/src/main/java/io/quarkiverse/googlecloudservices/firebase/admin/runtime/FirebaseAuthConfig.java index 03dddb46..241465b0 100644 --- a/firebase-admin/runtime/src/main/java/io/quarkiverse/googlecloudservices/firebase/admin/runtime/FirebaseAuthConfig.java +++ b/firebase-admin/runtime/src/main/java/io/quarkiverse/googlecloudservices/firebase/admin/runtime/FirebaseAuthConfig.java @@ -40,6 +40,13 @@ public interface AuthConfig { @WithDefault("true") boolean useEmulatorCredentials(); + /** + * When set, the values in this claim in the Firebase JWT will be mapped to the roles in the Quarkus + * {@link io.quarkus.security.identity.SecurityIdentity}. This claim can either be a set of roles + * (i.e. an array in the JWT) or a single value. + */ + Optional rolesClaim(); + } } diff --git a/firebase-admin/runtime/src/main/java/io/quarkiverse/googlecloudservices/firebase/admin/runtime/authentication/http/DefaultFirebaseIdentityProvider.java b/firebase-admin/runtime/src/main/java/io/quarkiverse/googlecloudservices/firebase/admin/runtime/authentication/http/DefaultFirebaseIdentityProvider.java index 887153b2..29ccf433 100644 --- a/firebase-admin/runtime/src/main/java/io/quarkiverse/googlecloudservices/firebase/admin/runtime/authentication/http/DefaultFirebaseIdentityProvider.java +++ b/firebase-admin/runtime/src/main/java/io/quarkiverse/googlecloudservices/firebase/admin/runtime/authentication/http/DefaultFirebaseIdentityProvider.java @@ -1,7 +1,7 @@ package io.quarkiverse.googlecloudservices.firebase.admin.runtime.authentication.http; import java.security.Principal; -import java.util.Optional; +import java.util.*; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; @@ -9,6 +9,7 @@ import com.google.firebase.auth.FirebaseAuth; import com.google.firebase.auth.FirebaseToken; +import io.quarkiverse.googlecloudservices.firebase.admin.runtime.FirebaseAuthConfig; import io.quarkus.security.identity.AuthenticationRequestContext; import io.quarkus.security.identity.IdentityProvider; import io.quarkus.security.identity.SecurityIdentity; @@ -25,6 +26,9 @@ public class DefaultFirebaseIdentityProvider implements IdentityProvider authenticate(FirebaseAuthenticationRequest request, * @param token The FirebaseToken to be authenticated. * @return A SecurityIdentity representing the authenticated user or null if authentication fails. */ - public static SecurityIdentity authenticate(FirebaseToken token) { + public SecurityIdentity authenticate(FirebaseToken token) { var builder = QuarkusSecurityIdentity.builder() .setPrincipal(getPrincipal(token)); + config.auth().rolesClaim().ifPresent(claim -> { + var claims = token.getClaims(); + if (claims.containsKey(claim)) { + var value = claims.get(claim); + var roles = getRolesFromClaimsValue(value); + builder.addRoles(roles); + } + }); + return builder.build(); } + @SuppressWarnings("unchecked") + private Set getRolesFromClaimsValue(Object value) { + if (value instanceof String) { + return Set.of((String) value); + } + + if (value instanceof Collection) { + return new HashSet<>((Collection) value); + } + + if (value instanceof String[]) { + return Set.of((String[]) value); + } + + throw new IllegalArgumentException("Unsupported value type: " + value.getClass()); + } + /** * Creates a FirebasePrincipal from the provided FirebaseToken. * diff --git a/integration-tests/firebase-admin/src/main/java/io/quarkiverse/googlecloudservices/it/firebaseadmin/FirebaseAppResource.java b/integration-tests/firebase-admin/src/main/java/io/quarkiverse/googlecloudservices/it/firebaseadmin/FirebaseAppResource.java index a15ffcb0..9e705617 100644 --- a/integration-tests/firebase-admin/src/main/java/io/quarkiverse/googlecloudservices/it/firebaseadmin/FirebaseAppResource.java +++ b/integration-tests/firebase-admin/src/main/java/io/quarkiverse/googlecloudservices/it/firebaseadmin/FirebaseAppResource.java @@ -1,5 +1,6 @@ package io.quarkiverse.googlecloudservices.it.firebaseadmin; +import jakarta.annotation.security.RolesAllowed; import jakarta.inject.Inject; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; @@ -32,4 +33,12 @@ public FirebaseOptions getSecretOptions() { return firebaseApp.getOptions(); } + @GET + @RolesAllowed({ "admin" }) + @Path("/admin-options") + @Produces(MediaType.APPLICATION_JSON) + public FirebaseOptions getAdminOptions() { + return firebaseApp.getOptions(); + } + } diff --git a/integration-tests/firebase-admin/src/main/resources/application.properties b/integration-tests/firebase-admin/src/main/resources/application.properties index bac883f6..8d04e203 100644 --- a/integration-tests/firebase-admin/src/main/resources/application.properties +++ b/integration-tests/firebase-admin/src/main/resources/application.properties @@ -2,4 +2,5 @@ # When the emulator is started with this project ID, non-emulated services access will fail. quarkus.google.cloud.project-id=demo-test-project-id quarkus.google.cloud.firebase.auth.enabled=true +quarkus.google.cloud.firebase.auth.roles-claim=roles quarkus.google.cloud.devservices.project-id=demo-test-project-id \ No newline at end of file diff --git a/integration-tests/firebase-admin/src/test/java/io/quarkiverse/googlecloudservices/it/firebaseadmin/FirebaseAuthResourceTest.java b/integration-tests/firebase-admin/src/test/java/io/quarkiverse/googlecloudservices/it/firebaseadmin/FirebaseAuthResourceTest.java index 71dbcc84..a5d0ccf1 100644 --- a/integration-tests/firebase-admin/src/test/java/io/quarkiverse/googlecloudservices/it/firebaseadmin/FirebaseAuthResourceTest.java +++ b/integration-tests/firebase-admin/src/test/java/io/quarkiverse/googlecloudservices/it/firebaseadmin/FirebaseAuthResourceTest.java @@ -8,15 +8,24 @@ import static org.hamcrest.Matchers.not; import java.util.Map; +import java.util.Set; + +import jakarta.inject.Inject; import org.junit.jupiter.api.Test; +import com.google.firebase.auth.FirebaseAuth; +import com.google.firebase.auth.FirebaseAuthException; + import io.quarkus.test.junit.QuarkusTest; import io.restassured.http.ContentType; @QuarkusTest class FirebaseAuthResourceTest extends FirebaseAuthTest { + @Inject + FirebaseAuth firebaseAuth; + @Test void shouldCreateUser() { given() @@ -62,4 +71,51 @@ void shouldStoreCustomUserClaims() { .body("customClaims", hasEntry("role", "admin")); } + @Test + void shouldHandleRoles() throws FirebaseAuthException { + given() + .contentType(ContentType.JSON) + .queryParam("uid", "6789") + .queryParam("email", "johndoe@quarkusio.com") + .queryParam("displayName", "John Doe") + .post("/auth/users/create") + .then() + .log().ifValidationFails() + .statusCode(200) + .body("uid", equalTo("6789")) + .body("customClaims", anEmptyMap()); + + given() + .contentType(ContentType.JSON) + .body(Map.of("roles", Set.of("admin"))) + .put("/auth/users/{uid}/claims", 6789) + .then() + .log().ifValidationFails() + .statusCode(204); + + var customToken = firebaseAuth.createCustomToken("6789"); + + var emulatorHostParts = emulatorHost.split(":"); + var port = emulatorHostParts.length == 2 ? Integer.parseInt(emulatorHostParts[1]) : 9099; + + var bodyAsJson = given() + .urlEncodingEnabled(false) + .port(port) + .contentType(ContentType.JSON) + .body(Map.of("token", customToken, "returnSecureToken", true)) + .log().all() + .post("identitytoolkit.googleapis.com/v1/accounts:signInWithCustomToken?key=test") + .jsonPath(); + var idToken = bodyAsJson.get("idToken"); + + given() + .header("Authorization", "Bearer " + idToken) + .get("/app/admin-options") + .then() + .log().ifValidationFails() + .statusCode(200) + .body("projectId", equalTo("demo-test-project-id")); + + } + }