Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Testing Identity Crisis - Unable to inject OIDC UserInfo into @TestSecurity #44824

Open
trixpan opened this issue Nov 28, 2024 · 11 comments
Open
Labels

Comments

@trixpan
Copy link
Contributor

trixpan commented Nov 28, 2024

Describe the bug

While coding a small demo in Quarkus I observed that while code like:

@Inject
SecurityIdentity securityIdentity;

// ...

for (Map.Entry<String, Object> entry : securityIdentity.getAttributes().entrySet()) {
///
}

will hit an instance of UserInfo when running against Keycloak in production, however, this behavior cannot be easily reproduced via @TestSecurity.

The issue seems to be that

@TestSecurity(
attributes = {
//
}

accepts KV of strings, and optionally a KV where type can be set via type = however, AttributeType only accept a range of types, none of which is UserInfo.

Ideally, given securityIdentity.getAttributes() may return userinfo with an instance of UserInfo, one should be able to do something like:

    @Test
    @TestSecurity(
            user = "testUser",
            roles = {"user", "admin"},
            attributes = {
                    @SecurityAttribute(key = "email", value = "[email protected]"),
                    @SecurityAttribute(key = "userinfo", value = "{\"sub\": \"ject\"}", type = AttributeType.USERINFO)
            }
    )

Expected behavior

Being able to inject userinfo attribute of UserInfo type as observed at run time.

Actual behavior

Unable to inject userinfo attribute

How to Reproduce?

Code is found above

Output of uname -a or ver

No response

Output of java -version

No response

Quarkus version or git rev

No response

Build tool (ie. output of mvnw --version or gradlew --version)

No response

Additional information

No response

@trixpan trixpan added the kind/bug Something isn't working label Nov 28, 2024
Copy link

quarkus-bot bot commented Nov 28, 2024

/cc @pedroigor (oidc), @sberyozkin (oidc,security)

@okarmazin
Copy link

okarmazin commented Nov 29, 2024

Quarkus has an annotation for that which you can use in conjunction with TestSecurity - OidcSecurity. It comes from extension io.quarkus:quarkus-test-security-oidc.

@TestSecurity(user = "testUser", roles = ["role1", "role2"])
@OidcSecurity(
    userinfo = [
        UserInfo(key = "sub", value = "subject"),
        UserInfo(key = "email", value = "[email protected]"),
        UserInfo(key = "name", value = "Test User"),
    ]
)
@Test
fun test() {...}

https://quarkus.io/guides/security-oidc-bearer-token-authentication#bearer-token-integration-testing-security-annotation

Unfortunately this information is hidden in the documentation in an unintuitive place - in a section called "OpenID Connect (OIDC) Bearer token authentication".

Security Testing doesn't mention it at all.

I would suggest to the Quarkus team members who read this message that OidcSecurity be added to the "Security Testing" documentation.

@trixpan
Copy link
Contributor Author

trixpan commented Nov 29, 2024

@okarmazin I suspect @OidcSecurity requires the use of an injected UserInfo or as the documentation you linked states:

public class ProtectedResource {

    @Inject
    JsonWebToken accessToken;
    @Inject
    UserInfo userInfo;
    @Inject
    OidcConfigurationMetadata configMetadata;

That is not what is being addressed in this discussion.

As mentioned in the OP,

    @Inject
    SecurityIdentity securityIdentity;

...
x = securityIdentity.getAttributes();

Will return Map<String, Object> with a key userinfo with value of type UserInfo being obtained without the separate injection of UserInfo userInfo; documented in your link.

@okarmazin
Copy link

okarmazin commented Nov 29, 2024

I suspect @OidcSecurity requires the use of an injected UserInfo

No, the UserInfo constructed from OidcSecurity is filled into your SecurityIdentity as attribute named "userinfo". You are not required to specifically inject a value of type UserInfo in order for OidcSecurity to work. You can inject SecurityIdentity as normal. The documentation just showcases injecting UserInfo directly as an example.

Injecting UserInfo directly is only a convenience which extracts the UserInfo from the current SecurityIdentity automatically and hands it to you. It is the same as if you injected SecurityIdentity and read the "userinfo" attribute manually.

@trixpan
Copy link
Contributor Author

trixpan commented Nov 29, 2024

I cannot reproduce what you describe

@QuarkusTest
@TestHTTPEndpoint(Profile.class)
class ProfileTest {
...
 @Test
    @TestSecurity(
            user = "testUser",
            roles = {"user", "admin"},
            attributes = {
                    @SecurityAttribute(key = "email", value = "[email protected]")
            })
    @OidcSecurity(
            userinfo = {
                    @io.quarkus.test.security.oidc.UserInfo(key = "key", value = "value")
            }
    )
    public void testProfileWithUserAndRolesAndAttributes() {
...

Image

@okarmazin
Copy link

How different is your setup from the following?

@Path("/api")
@Blocking
@Authenticated
class IdentityTestController(private val identity: SecurityIdentity) {
    @GET
    @Path("/identityTest")
    fun testIdentity() {
        val attrs = identity.attributes

        println(attrs)
    }
}


// ===========================

@QuarkusTest
class IdentityTest {

    @Test
    @TestSecurity(
        user = "testUser",
        roles = ["user", "admin"],
        attributes = [SecurityAttribute(key = "email", value = "[email protected]")]
    )
    @OidcSecurity(userinfo = [UserInfo(key = "key", value = "value")])
    fun testIdentity() {
        When {
            get("/api/identityTest")
        }
    }
}

//> {configuration-metadata=io.quarkus.oidc.OidcConfigurationMetadata@6aed7f73, [email protected], userinfo=io.quarkus.oidc.UserInfo@8a28302}

Image

@trixpan
Copy link
Contributor Author

trixpan commented Nov 29, 2024

Quite similar

@Path("/")
public class Profile {

    @Inject
    SecurityIdentity securityIdentity;

    @Inject
    Template profile; 

    @GET
    @Path("profile")
    @Authenticated
    @Produces(MediaType.TEXT_HTML)
    public TemplateInstance profile() {
        TemplateInstance instance = profile.data("identity", securityIdentity);

        // obtain roles from securityIdentity
        Map<String, Object> attributes = new java.util.HashMap<>(Map.of());
        if (securityIdentity.getRoles() != null) {
            attributes.put("roles", securityIdentity.getRoles().stream().toList());
        }
        if (securityIdentity.getPrincipal() != null) {
            attributes.put("name", securityIdentity.getPrincipal().getName());
        }

        // finally add remaining attributes
        attributes.putAll(handleAttributes(securityIdentity.getAttributes()));

        instance = instance.data("attributes", attributes);

        return instance;
    }
...

@sberyozkin
Copy link
Member

@trixpan @TestSecurity on its own can not to support extension specific authentication mechanisms. You need to bring in OIDC specific test support, and use @OidcSecurity, see https://quarkus.io/guides/security-oidc-bearer-token-authentication#bearer-token-integration-testing-security-annotation

@sberyozkin
Copy link
Member

@trixpan Can you please clarify again what exactly you are trying to reproduce in the test, and what is not working when you try to use @OidcSecurity as suggested by @okarmazin ?

@trixpan
Copy link
Contributor Author

trixpan commented Nov 29, 2024

@sberyozkin

securityIdentity.getAttributes() does not include "userinfo": UserInfo when running against a like Keycloak instance the `"userinfo: UserInfo" item is returned by the same call.

@sberyozkin
Copy link
Member

sberyozkin commented Nov 29, 2024

@trixpan I've added a simple unit test to confirm the userinfo attribute is set correctly: #44835.

We also have @TestSecurity setups which support UserInfo injection - which can only succeed if this attribute is set correctly, see this code

Can you review your test setup again and if you can't get the test working, please create a simple reproducer for me to have a look

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

3 participants