Skip to content

Commit

Permalink
Merge pull request #725 from jfbenckhuijsen/feature/firebase-auth-roles
Browse files Browse the repository at this point in the history
Feature/firebase auth roles
  • Loading branch information
loicmathieu authored Dec 30, 2024
2 parents 8da70d7 + 1f3de76 commit 384ee7e
Show file tree
Hide file tree
Showing 7 changed files with 139 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
|

|===

Original file line number Diff line number Diff line change
Expand Up @@ -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
|

|===

Original file line number Diff line number Diff line change
Expand Up @@ -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<String> rolesClaim();

}

}
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
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;

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;
Expand All @@ -25,6 +26,9 @@ public class DefaultFirebaseIdentityProvider implements IdentityProvider<Firebas
@Inject
FirebaseAuth auth;

@Inject
FirebaseAuthConfig config;

/**
* Retrieves the request type that this provider supports.
*
Expand Down Expand Up @@ -65,13 +69,39 @@ public Uni<SecurityIdentity> 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<String> getRolesFromClaimsValue(Object value) {
if (value instanceof String) {
return Set.of((String) value);
}

if (value instanceof Collection) {
return new HashSet<>((Collection<String>) 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.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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", "[email protected]")
.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"));

}

}

0 comments on commit 384ee7e

Please sign in to comment.