haskell-servant / servant-auth

160 stars 73 forks source link

JWT token validation without signing #121

Open Unisay opened 6 years ago

Unisay commented 6 years ago

In the microservice infrastructure I am developing JWT tokens are issued by microservice A and validated by microservices B and C. When developing B and C I wan't to configure servant-auth-server to only validate tokens and never sign. Given how JWTSettings data type is defined (signingKey is mandatory) - I don't see how I could achieve my goal:

-- | @JWTSettings@ are used to generate cookies, and to verify JWTs.
data JWTSettings = JWTSettings
  {
  -- | Key used to sign JWT.
    signingKey      :: Jose.JWK
  -- | Algorithm used to sign JWT.
  , jwtAlg          :: Maybe Jose.Alg
  -- | Keys used to validate JWT.
  , validationKeys  :: Jose.JWKSet
  -- | An @aud@ predicate. The @aud@ is a string or URI that identifies the
  -- intended recipient of the JWT.
  , audienceMatches :: Jose.StringOrURI -> IsMatch
  } deriving (Generic)

The questions are: How is it possible to address aforementioned use-case with the current state of the code, and if its not possible, would you consider validation-only use-case to be implemented in the future?

domenkozar commented 6 years ago

signingKey is only used in makeJWT, which you can decide just to never call. So for now you'll need to call defaultJWTSettings with a key to generate the signing key, but it would never be used.

Unisay commented 6 years ago

Thank you, I understand that the approach you suggested would work. Do you regard it as a long term solution or as a temporary workaround?

(The first think that comes to mind is to model signingKey as Maybe Jose.JWT)

alpmestan commented 6 years ago

I don't want to answer in @domenkozar's name, so this is just my personal opinion: I think we're always open to improvements. So if you have an idea for making this a little less confusing and maybe a little more elegant, we'd be interested in hearing about it. Always. :-)

ProofOfKeags commented 5 years ago

+1 for this. I think it's a common enough pattern that the service issuing the auth token might be different than the services validating the auth token, in which case you don't want to require the private key being onboard the validating services.

nmattia commented 5 years ago

@domenkozar could we add this to the README somewhere?

signingKey is only used in makeJWT, which you can decide just to never call. So for now you'll need to call defaultJWTSettings with a key to generate the signing key, but it would never be used.

that's what I suspected but I'm glad I found this ticket to confirm.

nmattia commented 5 years ago

Actually I'm confused again:

(n ~ S (S Z), HasServer (AddSetCookiesApi n api) ctxs, AreAuths auths ctxs v, HasServer api ctxs, AddSetCookies n (ServerT api Handler) (ServerT (AddSetCookiesApi n api) Handler), ToJWT v, HasContextEntry ctxs CookieSettings, HasContextEntry ctxs JWTSettings) => HasServer (Auth auths v :> api :: Type) ctxs

Why does v (in Auth auths v) need to be ToJWT?

From skimming the code it looks like the HasServer instance will set some cookies with the newly encoded JWT. How can I work around this and/or recover the original JWT (ideally without adding an IORef to the Context)?

nmattia commented 5 years ago

Related: https://github.com/haskell-servant/servant-auth/issues/40 #119

freckletonj commented 5 years ago

Relatedly, I validate another service's JWTs, and I do so by polling for their signing key because it occasionally changes.

Is there a way to define Context '[TVar JWTSettings] so that I can validate JWTs based on an updateable key?

Or another way of addressing this, short of restarting the WAI app every time the key changes?

nmattia commented 5 years ago

Had the same use case, this might help: https://github.com/deckgo/deckdeckgo/blob/4c61f0f366e15cf5097bb947294d519473d92b85/infra/firebase-login/src/Servant/Auth/Firebase.hs#L53

freckletonj commented 5 years ago

@nmattia Thanks! I've been trying to get this to work for the past day, and, whew. Servant. Tough Types.

In the end, I just used endpoints authed by Auth auths User

An uninhabited type like data TVarJWT

A fn like tVarJwtAuthCheck :: FromJWT usr => TVar JWTSettings -> AuthCheck usr

and an IsAuth instance like:

instance FromJWT usr => IsAuth TVarJWT usr where
  type AuthArgs TVarJWT = '[TVar JWTSettings]
  runAuth _ _ = tVarJwtAuthCheck

The one catch with doing things this way Servant's instance for HasServer ... Auth auths a :> sub requires HasContextEntry for CookieSettings and JWTSettings, meaning, even if you don't want them, you have to have them if you want to use the whole Auth auths User pattern.

I started writing a more general HasServer instance, but got super swamped. So. Hopefully this helps a future person mired in types.

alpmestan commented 5 years ago

Yes, this is all a bit too strict to our taste, and there's a "roadmap" to address this: https://summer.haskell.org/ideas.html#servant-auth-improvements