kaleidos / grails-security-stateless

Grails plugin to implement stateless authentication using Spring Security
Apache License 2.0
17 stars 8 forks source link

Is it possible to use a parametric secretKey? #38

Open ppazos opened 8 years ago

ppazos commented 8 years ago

Right now the secret key is configured globally in Config.groovy

I have a requirement of validating tokens generated externally, using a secret key that I provide. Those keys are generated for different users of the app, and provider to them (it is an API key).

My users will generate JWT using that key, and send the JWT on every request. Since I have the key, and receive the token, I want to validate it.

Is it possible to tell the security stateless plugin which private key to use dynamically to not use the global one from Config.groovy?

Thanks

ppazos commented 8 years ago

Dear @pabloalba @Alotor @mgdelacroix @burtbeckwith can you give some feedback about this issue?

Thanks a lot!

Alotor commented 8 years ago

I've checked this and what I think we should do is provide extension methods for CryptoService

Then you'll be able to change the default behaviour easily. You'll need to create a new class that extends CryptoService and override the getSecret method to use your own logic instead of using a configuration parameter.

What do you think? Would be this enough?

ppazos commented 8 years ago

@Alotor I think that should be enough if we can get the request.securityStatelessMap from the service that will extend CryptoService, because the secretKey will depend on information associated with the user, contained in the token payload. Is that possible?

I think that would need the plugin to parse the token payload before validating the token (don't know how it is currently done), I mean:

  1. my requirement: one user can be associated to N organizations, each organization has an "apikey"
  2. login (user, pass, org)
  3. generate and retrieve token (payload = user, org)
  4. request from client with token (payload = user, org)
  5. process payload, get user and org from request
  6. get secret key (apikey) for the org
  7. verify token (apikey)
  8. allow or deny request based on token verification
  9. & 5. should be the logic of the overwritten getSecret method you mentioned.

Does it sound reasonable?

ppazos commented 7 years ago

@Alotor @pabloalba any comments on ^?

I will try to accomplish this next week since I need it in my project: https://github.com/ppazos/cabolabs-ehrserver/issues/450

Since my app is multi-tenant, I don't want tokens from different organizations of be verified using the same "secret", but have a "secret" per organization.

Alotor commented 7 years ago

Hi @ppazos,

First, I apologize for not answering your queries. I'll try to be more over things from now on.

I understand what you're trying to achieve.

You need to extend JwtStatelessTokenProvider so the generateToken adds the organization and encrypt the token using your custom keys and validateAndExtractToken reads your token and decrypt with your custom keys.

Changes that will be required in the plugin:

When the above is done, you can implement your "MultiTentant" custom provider like:

class CustomJwtStatelessTokenProvider implements StatelessTokenProvider {
    Map<String, String> customKeys
    Integer expirationTime

    @Override
    void init(Integer expirationTime) {
        this.expirationTime = expirationTime
    }

    private getCompanyTokenProvider(String company) {
        def companyKey = customKeys[company]
        def companyCS = new CryptoService()
        companyCS.init(companyKey)

        def jwtProvider = new JwtStatelessTokenProvider()
        jwtProvider.init(this.expirationTime)
        jwtProvider.cryptoService = companyCS
    }

    @Override
    String generateToken(String userName, String salt=null, Map<String,String> extraData=[:]) {
        def company = extraData["company"]
        def jwtProvider = getCompanyTokenProvider(company)
        return jwtProvider.generateToken(userName, salt, extraData)
    }

    @Override
    Map extractToken(String token) {
        return TokenUtils.parseTokenData(token)
    }

    @Override
    void validateToken(String token, Map<String, String> extraData=[:]) {
        def company = extraData["company"]
        def jwtProvider = getCompanyTokenProvider(company)
        jwtProvider.validateToken(token, extraData)
    }
}

What do you think? Will this be enough?

ppazos commented 7 years ago

@Alotor awesome! thanks for the detailed guide. I will try to implement this and get back to you if I have any issues. I'll send my changes to the plugin as a PR.

About the extraData, I my idea was to put the organization UID there, so I can get the organization by UID before validating the token, and get the "secret" from the organization, to validate the token (I don't want to use the org UID as "secret" because it is public, the "secret" would be an private API key or something like that... an UID).

One small question, where/how should I inject my implementation of the StatelessTokenProvider so the plugin takes my custom provider instead of the default one?

Thanks again!