strimzi / strimzi-kafka-oauth

OAuth2 support for Apache Kafka® to work with many OAuth2 authorization servers
Apache License 2.0
144 stars 89 forks source link

Map Group to User Principal using data in User Info Endpoint #192

Open slecrenski opened 1 year ago

slecrenski commented 1 year ago

Is it possible or can it be eventually possible to use claims found in the User Info Endpoint during authentication to map groups to a user principal? For example if the User Info Endpoint responds with a "groups" key in the json response containing an array of groups the user should be in.

{
"groups": ["group1", "group2", "group3"]
}

Is there a way to configure this module to use these groups found in the User Info endpoint to map groups to a user principal? Some identity providers only put groups in this endpoint and are not found nor will they ever be found in the access tokens.

I see you have configuration to set User Info Endpoint but perhaps a configuration setting in the jaas config could trigger you to prioritize User Info for group mapping. I would suspect this wouldn't be all that difficult.

Also is it possible to use the User Info Endpoint to obtain the preferred_username? Or at least configure the User Info Endpoint to be priority when deciding on the User Principal. Even though I have the User Info Endpoint configured it doesn't seem to actually use it at all. The preferred_username is not in the access token and it is not retrievable currently by using a scope.

Thanks!

mstruk commented 1 year ago

The current group mapping support requires you to use a custom written authorizer that knows how to extract the mapped information from OAuthKafkaPrincipal object and do something useful with it. See README here. It means these groups are not visible to standard Kafka ACL authorizer. Groups mapping should use the User Info Endpoint response if the groups information is not found initially.

You can already map any response from User Info Endpoint to OAuthKafkaPrincipal groups. See oauth.username.claim, oauth.fallback.username.claim and oauth.fallback.username.prefix for mapping the username. But you have to use Introspection Endpoint and specify oauth.userinfo.endpoint.uri for fallback to User Info Endpoint to occur.

There was an issue with nested claims when mapping the username, but that should be fixed in the next release. See #194 .

slecrenski commented 1 year ago

Understood on requirement to write custom authorizer. Plan on using group prefix naming conventions to authorize requests for simplicity of authorization with sufficient level of granularity defined in group suffix naming. The groups attribute are not or do not appear to be populated in the OAuthKafkaPrincipal during authentication. I have confirmed because it is null in my authorizer code. The groups in our system are defined in only the UserInfo endpoint. They are not in the access token for reasons of token size since one might have a significant number of groups. I strongly suspect that the strimzi-kafka-oauth JaasServerOauthValidatorCallbackHandler and JaasServerOauthOverPlainValidatorCallbackHandler are not considering the UserInfo endpoint to populate groups during authentication. Obviously I don't want to have to pull groups from the UserInfo endpoint on every request or even do my own caching of groups in the authorizer. Can you double check the code actually uses the userinfo endpoint if defined to populate groups? So the access token should be used to query the userinfo endpoint to retrieve additional metadata asked for in the initial scope defined during authentication. It would also be great if the custom claim check would also consider claims in the UserInfo endpoint instead of the access token. I'm not entirely convinced the username fallback is even considering the UserInfo endpoint as well.

oauth.userinfo.endpoint.uri="https://url" \
oauth.groups.claim="$.groups" 

The groups do not use comma for delimiter. Your framework seems to default oauth.groups.claim.delimiter to ","

Is there a way to set this to array of strings?

{
"groups": ["group1", "group2", "group3"]
}

Thanks!

mstruk commented 1 year ago

Can you share your listener oauth / jaas configuration?

UserInfo endpoint should be used when Introspection endpoint is configured. It will not be used when JWKS endpoint is configured.

slecrenski commented 1 year ago

Yes I have confirmed I am using the Introspection Endpoint. Please find the filtered configuration below.

listener.name.clientoap.sasl.enabled.mechanisms=PLAIN
listener.name.clientoap.oauthbearer.connections.max.reauth.ms=3600000
listener.name.clientoap.plain.sasl.server.callback.handler.class=io.strimzi.kafka.oauth.server.plain.JaasServerOauthOverPlainValidatorCallbackHandler
listener.name.clientoap.plain.sasl.jaas.config=org.apache.kafka.common.security.plain.PlainLoginModule required \
  oauth.client.id="{FILTERED}" \
  oauth.client.secret="{FILTERED}" \
  oauth.token.endpoint.uri="{FILTERED}" \
  oauth.introspection.endpoint.uri="{FILTERED}" \
  oauth.valid.issuer.uri="{FILTERED}" \
  oauth.userinfo.endpoint.uri="{FILTERED}" \
  oauth.username.claim="preferred_username"  \
  oauth.fallback.username.claim="preferred_username" \
  oauth.scope="openid profile email groups preferred_username group:domain/\groupwhichhasaccess" \
  oauth.ssl.truststore.location="truststore.p12" \
  oauth.ssl.truststore.type="PKCS12" \
  oauth.ssl.truststore.password="{FILTERED}" \
  oauth.valid.token.type="Bearer" \
  oauth.groups.claim="$.groups" \
  oauth.custom.claim.check="@.scope == 'openid profile email groups preferred_username group:domain/\groupwhichhasaccess'" \
  unsecuredLoginStringClaim_sub="unused" \
  oauth.ssl.endpoint.identification.algorithm="";
DEBUG Configured OAuthIntrospectionValidator:
    introspectionEndpointUri: {FILTERED}
    sslSocketFactory: com.ibm.jsse2.SSLSocketFactoryImpl@{FILTERED}
    hostnameVerifier: io.strimzi.kafka.oauth.common.SSLUtil$${FILTERED}
    principalExtractor: PrincipalExtractor {usernameClaim: preferred_username, fallbackUsernameClaim: preferred_username, fallbackUsernamePrefix: null}
    groupsClaimQuery: $.groups
    groupsClaimDelimiter: null
    validIssuerUri: {FILTERED}
    userInfoUri: {FILTERED}
    validTokenType: null
    clientId: {FILTERED}
    clientSecret: {FILTERED}
    audience: null
    customClaimCheck: @.scope == 'openid profile email groups preferred_username group:domain/groupwhichhasaccess'
    connectTimeoutSeconds: 60
    readTimeoutSeconds: 60 (io.strimzi.kafka.oauth.validator.OAuthIntrospectionValidator)

I see no DEBUG log mentioning any call out to an introspection endpoint or a user info endpoint for anything albeit it appearing to be configured correctly and detecting correct configuration in DEBUG logs.

mstruk commented 1 year ago

Note, that if preferred_username attribute is found in the response of Introspection Endpoint there will be no attempt to fetch from User Info Endpoint.

slecrenski commented 1 year ago

How does that help my requirement? I guess I could update the groups object in the OAuthKafkaPrincipal during initial authorization events in my custom authorizer. Hopefully it’s mutable. If not I’ll probably just write my own authentication and authorization module.

mstruk commented 1 year ago

Try set: oauth.username.claim="doesnotexist"

slecrenski commented 1 year ago

Ok so that did pickup the groups during authentication event. So I guess I can go with that for now. Seems a bit hacky tho..? Hopefully things will continue to work in the future after upgrades. I guess worst case is user wouldn't be able to be authorized or even authenticated to Kafka. Which would be a bit of a problem. Perhaps there might be a way to redesign the configuration to be more explicit about which route is chosen for various claims.

mstruk commented 1 year ago

It's a very niche use-case so it hasn't seen much feedback from the field. My assumption was that if username claim is available in Introspection Endpoint Response then groups info would be as well. The behaviour though is as intended so I don't see why we would want to break backwards compatibility in the future. For starters it would be worth describing in the docs how to configure for your use case.

I agree that it would make sense to make this config less hacky. If possible that would not involve adding another config option, as each new option adds to the already pretty long list of them. For example, we could simply check if groups extraction is configured, and if no match on Introspection Endpoint result, we could perform User Info Endpoint request automatically.

Do you have some specific proposal?