ch4mpy / spring-addons

Ease spring OAuth2 resource-servers configuration and testing
Apache License 2.0
559 stars 90 forks source link

Define a way with `WithMockKeycloakAuth` to populate accessToken information #14

Closed SrMouraSilva closed 3 years ago

SrMouraSilva commented 4 years ago

In the version 2.1.0, is possible to define sessionState

@WithMockKeycloakAuth(authorities=["something"]), accessToken=WithAccessToken(sessionState="00000000-0000-0000-0000-000000000000")

But in the version 2.4.0, I don't found a way to define AccessToken.sessionState (session_state). Also, I need define too AccessToken.id (jti) and AccessToken.subject (sub).

I found how to define the sub, but not others

@WithMockKeycloakAuth(authorities=[CLASSROOM_TURMAS_CADASTRAR_COMANDO], id=IdTokenClaims(sub="00000000-0000-0000-0000-000000000000"))

Maybe a way to do this is defining a hashmap or something like to add all the (custom) token values that are needed

ch4mpy commented 4 years ago

In 2.3.0, I reorganized the properties, but this is mostly code shuffling, you should have about the same configuration options. It is possible I lost a few very low-level JWT claims like jti in the process, but see at the end of this post how you can set it in privateClaims). How comes your @Controller code (or @Service or any other "business" code) needs access to such claims (session_state and jti)? Shouldn't this be used by upstream frameworks only (like Spring-security, before access is granted and KeycloakAuthenticationToken is build)?

The reason for this change is I chose to get closer to the OpenID spec to re-use some code I wrote for another OpenID Authentication implementation (I personally don't use KeycloakAuthenticationToken any more mostly because Keycloak Spring libs are moving too slow). This spec is there https://openid.net/specs/openid-connect-core-1_0.html

Current state:

What this means for you:

As usual, samples are rather informative:

    @Test
    @WithMockKeycloakAuth(
            authorities = { "USER", "AUTHORIZED_PERSONNEL" },
            id = @IdTokenClaims(sub = "42"),
            oidc = @OidcStandardClaims(
                    email = "ch4mp@c4-soft.com",
                    emailVerified = true,
                    nickName = "Tonton-Pirate",
                    preferredUsername = "ch4mpy"),
            accessToken = @KeycloakAccessToken(
                    realmAccess = @KeycloakAccess(roles = { "TESTER" }),
                    authorization = @KeycloakAuthorization(
                            permissions = @KeycloakPermission(rsid = "toto", rsname = "truc", scopes = "abracadabra"))),
            privateClaims = @ClaimSet(stringClaims = @StringClaim(name = "foo", value = "bar")))
    public void whenAuthenticatedWithKeycloakAuthenticationTokenThenCanGreet() throws Exception {
        api.get("/greet")
                .andExpect(status().isOk())
                .andExpect(content().string(startsWith("Hello ch4mpy! You are granted with ")))
                .andExpect(content().string(containsString("AUTHORIZED_PERSONNEL")))
                .andExpect(content().string(containsString("USER")))
                .andExpect(content().string(containsString("TESTER")));
    }
SrMouraSilva commented 4 years ago

Hi, thanks for the response!

How comes your @controller code (or @service or any other "business" code) needs access to such claims (session_state and jti)? Shouldn't this be used by upstream frameworks only (like Spring-security, before access is granted and KeycloakAuthenticationToken is build)?

I create a audit Filter what use this information by auditing purposes. In fact this fields are not relevant by the test, but the object is used to other things, including a way to obtained the user data. As this three fields are always informed by Keycloak, I defined they as not null in my Kotlin code

It is possible I lost a few very low-level JWT claims like jti in the process, [...]

I don't found any reference in the current branch to jti by the Github seach

--

Ok, now I change to the similar example,

@WithMockKeycloakAuth(
            authorities=[CLASSROOM_TURMAS_CADASTRAR_COMANDO],
            id=IdTokenClaims(sub="00000000-0000-0000-0000-000000000000"),
            oidc = OidcStandardClaims(),
            privateClaims = ClaimSet(stringClaims=[
                StringClaim(name = "jti", value = "00000000-0000-0000-0000-000000000000"),
                StringClaim(name = IDToken.SESSION_STATE, value = "00000000-0000-0000-0000-000000000000")
            ])
    )

but this privateClaims only populate otherClaims image

This spec is there https://openid.net/specs/openid-connect-core-1_0.html

session_scope can be found in https://openid.net/specs/openid-connect-session-1_0.html#CreatingUpdatingSessions, but is a draft


Congratulations for the awesome project :D !

ch4mpy commented 4 years ago

I re-open. Further investigations needed

ch4mpy commented 3 years ago

Renamed privateClaims to otherClaims in @WithMockKeycloakAuth for clarity: Keycloak's token model does not support private claims at root as per JWT spec. Instead, it maps unknown claims to otherClaims properties.

Added jti and nbf (from JWT spec) to @IdTokenClaims (an ID token is a JWT)

Also added session_state to @IdTokenClaims as per https://openid.net/specs/openid-connect-session-1_0.html#CreatingUpdatingSessions

Sample usage:

    @WithMockKeycloakAuth(
            authorities = { "USER", "AUTHORIZED_PERSONNEL" },
            id = @IdTokenClaims(
                    sub = "42",
                    jti = "123-456-789",
                    nbf = "2020-11-18T20:38:00Z",
                    sessionState = "987-654-321"),
            oidc = @OidcStandardClaims(
                    email = "ch4mp@c4-soft.com",
                    emailVerified = true,
                    nickName = "Tonton-Pirate",
                    preferredUsername = "ch4mpy"),
            accessToken = @KeycloakAccessToken(
                    realmAccess = @KeycloakAccess(roles = { "TESTER" }),
                    authorization = @KeycloakAuthorization(
                            permissions = @KeycloakPermission(rsid = "toto", rsname = "truc", scopes = "abracadabra"))),
            otherClaims = @ClaimSet(stringClaims = @StringClaim(name = "foo", value = "bar")))

Please re-open if you find other missing claims.

P.S. @SrMouraSilva sorry for abnormally long processing. I got caught by urgent stuff at work, then changed my laptop (and had to re-install everything) and then just ... forgot about this opened issue => so very, very sorry :(

SrMouraSilva commented 3 years ago

@SrMouraSilva sorry for abnormally long processing. I got caught by urgent stuff at work, then changed my laptop (and had to re-install everything) and then just ... forgot about this opened issue => so very, very sorry :(

Hi ch4mpy! I completely understand the situation. Thank you for your availability! I am updating my code.