Skip to content
This repository has been archived by the owner on Oct 29, 2021. It is now read-only.

Verification of Tokens from external Auth Provider (Keycloak) #175

Closed
anmolitor opened this issue Sep 21, 2020 · 9 comments
Closed

Verification of Tokens from external Auth Provider (Keycloak) #175

anmolitor opened this issue Sep 21, 2020 · 9 comments

Comments

@anmolitor
Copy link

Hello,
I am trying to set up a Haskell Web Server where some endpoints are protected by Keycloak.
However, I cannot get it to work, any valid access token gets converted into "Indefinite".

My current process is:

  • I get the JWKSet from the Keycloak Server (RS256 algorithm) via Servant-Client. This works as expected, the request goes through without issues.

  • I build the JWTSettings needed for Servant-Auth by starting with default settings and then adding the JWKSet from the previous step as "validationKeys" and the first member of the set as signing key (as far as i understand JWT token validation without signing #121 the signing key ends up unused anyways if I only verify).

  • I use defaultCookieSettings as I do not intend to use Cookies

  • I add Auth '[JWT] AuthUser to a route to protect it. AuthUser is a record with a single field "preferred_username" since that is the part of the claims I currently need.

The Code compiles, but requests to the protected path get blocked by the authorization, although
the header Authorization: Bearer <token> is set with a newly generated access token.
There is no error log, the only clue is that the AuthResult is Indefinite.

Can you give me a clue? What I am doing wrong? Is there an easy way to debug this?

@cdupont
Copy link

cdupont commented Oct 14, 2020

Hi @andreasewering, did you solve your issue? I have the same problem.
I get the RSA "public key" from the Keycloak interface. It is of the form: "MIIBIjANBgkqhkiG9w....."
I insert it in my code this way:

let myKey = fromSecret "MIIBIjANB..."
     jwtCfg = defaultJWTSettings myKey
     cfg = defaultCookieSettings :. jwtCfg :. EmptyContext 
     api = Proxy :: Proxy (API '[JWT])
     context = Proxy :: Proxy '[CookieSettings, JWTSettings]
     server' = hoistServerWithContext api context myHandler server

However, I only get 401 responses (with correct JWTs)...

@cdupont
Copy link

cdupont commented Oct 14, 2020

OK, I think I solved it...
I get the key from this URL: https://example.com/auth/realms/my-realm/protocol/openid-connect/certs
Decoded this way:

let jsonKey = "{\"kid\":\"Lla9_pMxUpsT1Mzl3glvDELVHknz9fCo_uCrax8uvBw\",\"kty\":\"RSA\",\"alg\":\"RS256\",\"use\":\"sig\",\"n\":\"1GD_Zs4DaccHzZlWNq74MqPwy02sWNJMXcGB0bsbWtj-oX4AzZgUIniu60I3zOVLst8z    c16FzRg_vRfPSxMb-oKuRinfhKZJiZepXDi27bcsUvteprHLW8LHufvngNNzHNmrGjPsxEdhb9Mouw8DRmx6m_PgIYTv4ilSkCgNs3NteCRtDZl8_iSA0fpPwA77BV8mT8RBntZ6CbjV-zxGEsQ7ly5rAmG7ADUPFhOzM7DNDVbSAzOyK7OL5W7p0KFfpX8wphvoix2c    2hAjjjhCKHSqlm88DjfnXFm6ggYbY8_TC7ZwPB_Y8xQ3mM7-vEFSnmtIrPH089kw8HWnyntCPQ\",\"e\":\"AQAB\"}"
let myKey = case decode jsonKey of
      Nothing -> error "not decoded"
      Just e -> e

@domenkozar
Copy link
Collaborator

You should be able to do that by implementing your own FromJWT instance

@centromere
Copy link

@domenkozar If he's going to a JWK, a FromJSON instance is already available.

@cdupont
Copy link

cdupont commented Oct 15, 2020

Yes, I use this instance for JWK. For tokens, I'll make my own instance to decode my claims.

@anmolitor
Copy link
Author

anmolitor commented Oct 16, 2020

@cdupont I was not able to solve my issue. Since it is a test realm keycloak, I might as well paste some stuff here.
The JWK Json I get from the keycloak server is the following:
{"keys":[{"kid":"h-6n2OHsUA6ZthCxokXKpY92bwToHiUZrFCV69dcK6o","kty":"RSA","alg":"RS256","use":"sig","n":"nAoT69pwZKjiHkRbjkFyK2_6AdacYnINftrM7kLKcNxU9Lpx316qw-OS_PMglFD-nmSpN57f84fYGtv-WZmHinJitm-W02th-qdEkeZeRxUsV17ro82hPf4KC9yWDhE7i31rqB7umAuDtxTPOY9VmneTqPOThqL0YRFzodXn0nPm5JOEympsPFhIEGGOBiXGTH0VMnugQfc0XSTeSon9BLsuKBYUpk1sEoDTMGqWuwKTxaKM5wz7H0pJdEnfhQVWkcvLnA6l08TO50TTV8wUjwiBder9OD_6xKG1mMo7HRzdtp91IryNF3Dj-W6TCQrKR2cXZkz2APpFng_u3VJsaw","e":"AQAB"}]}

I send the request and automatically decode to a JWKSet using servant client. Printing the result gives me
JWKSet [JWK {_jwkMaterial = RSAKeyMaterial (RSAKeyParameters {_rsaN = Base64Integer 19698145131774439608660772646111743099858 40103133087456933326307456723963674469049969337611706316020530483739141027419070425412302779565743486118736071634368212932811 08950644767864187013098133988506593992924841503721006489274644418300479902163631616068886394230291731856558205703815361958757 59630640869574685087001441605386687119367864791914118813231997111658391710690999324645684846728969550473894658930671359232210 85354855362772687027267560183427356593629835616403505875681212416516789188168697701175868748424825036372810679065679263950500 6275969219815343385063613559763779384616038276676378835913595678653863324779, _rsaE = Base64Integer 65537, _rsaPrivateKeyPara meters = Nothing}), _jwkUse = Just Sig, _jwkKeyOps = Nothing, _jwkAlg = Just (JWSAlg RS256), _jwkKid = Just "h-6n2OHsUA6ZthCx okXKpY92bwToHiUZrFCV69dcK6o", _jwkX5u = Nothing, _jwkX5cRaw = Nothing, _jwkX5t = Nothing, _jwkX5tS256 = Nothing}]

I then create my settings, for now using unsafeHead

`firstOfJwkSet :: JWKSet -> JWK
firstOfJwkSet (JWKSet jwks) =
unsafeHead jwks

jwkSetToSettings :: JWKSet -> JWTSettings
jwkSetToSettings jwkSet =
(defaultJWTSettings $ firstOfJwkSet jwkSet) { validationKeys = jwkSet, jwtAlg = Just RS256 }`

I also tried not overwriting validationKeys or jwtAlg, but I got the same result.

My endpoint just looks like this right now:
`Auth '[JWT] AuthUser :> Get '[JSON] String

auth :: AuthResult AuthUser -> AppM String
auth (Authenticated usr) = return $ preferred_username usr
auth other = do
  print other
  return "error"

data AuthUser = AuthUser
{ preferred_username :: String
}
deriving (Generic, FromJSON, FromJWT, ToJSON, ToJWT, Show)

`

Example token (paste in jwt.io to see the claims): eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJoLTZuMk9Ic1VBNlp0aEN4b2tYS3BZOTJid1RvSGlVWnJGQ1Y2OWRjSzZvIn0.eyJqdGkiOiIxMTAzNzc3Mi1iZjQ4LTQxZjQtYTQzYi1kMzJiYTY4NmQ0NGUiLCJleHAiOjE2MDI4MjY3OTUsIm5iZiI6MCwiaWF0IjoxNjAyODI2NDk1LCJpc3MiOiJodHRwczovL2F1dGguYXBwcy5hbmRyZW5hLmRlL2F1dGgvcmVhbG1zL3Rlc3QiLCJhdWQiOiJ0ZXN0Y2xpZW50Iiwic3ViIjoiMGVlNjQ0YTQtY2M5MC00YTFlLTlmYjgtMmNmNjI2NzFiNWMxIiwidHlwIjoiQmVhcmVyIiwiYXpwIjoidGVzdGNsaWVudCIsImF1dGhfdGltZSI6MCwic2Vzc2lvbl9zdGF0ZSI6ImQyYzk4NmZjLTJmMGUtNDYxMi04YjNhLWFlNzU0ZDE4ZjllZSIsImFjciI6IjEiLCJhbGxvd2VkLW9yaWdpbnMiOltdLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsib2ZmbGluZV9hY2Nlc3MiLCJ1bWFfYXV0aG9yaXphdGlvbiJdfSwicmVzb3VyY2VfYWNjZXNzIjp7ImFjY291bnQiOnsicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyIsInZpZXctcHJvZmlsZSJdfX0sInNjb3BlIjoiIiwibmFtZSI6IlRpYmVyaXVzIEVzdG9yIiwicHJlZmVycmVkX3VzZXJuYW1lIjoidGVzdCIsImdpdmVuX25hbWUiOiJUaWJlcml1cyIsImZhbWlseV9uYW1lIjoiRXN0b3IifQ.jKfS59uyeWC3j8ovrV91BwWuRCOC6z3jhZdeSZFgUyA6XB-7eGT-pv6vEzYLYPI47mba2GIdSt1WtB7s4s_QW2vmFETj2lZ9SmDf4NvukQWjpUlr7E3SueYZpwl69-BOFnxjWFdzTqEKC4cGbIdlJzzDBXa3nXe0JTLyFt9b9G2O_Bej503fyhJecUhTw3kVo9FBE1D9ntYbueSsTozq0dgymvjM91tK78lh9xD_7KBj6InlE_FuJDhFFgBuAnql-5J8V9xo2mhSfikMeRpdMx4YeS7hpBHdk415S3_jBUxRQibBzvbjRjYXYL_OiDHF5NIAghVbWEZHdAhUtwEHGQ

@anmolitor
Copy link
Author

anmolitor commented Oct 16, 2020

@cdupont How did you solve it in your case? Is the derived FromJWT instance the problem?

@cdupont
Copy link

cdupont commented Oct 16, 2020

@andreasewering I looks all good to me. Some ideas: why not decoding directly to a JWK instead of going through a JWKSet (like in my example above)?
I wonder what is failing in your case, is it the signature checking, wrong auth method, or claims decoding?
@domenkozar it would be nice if the errors would be more explicit, what do you think?

@anmolitor
Copy link
Author

anmolitor commented Oct 16, 2020

I figured it out. It seems to be a problem with the default FromJWT instance, that assumes that claims go in a 'dat' field of the decoded jwt. Using dummy instances as follows
`instance FromJWT Jose.ClaimsSet where
decodeJWT = Right

instance ToJWT Jose.ClaimsSet where
encodeJWT = id `

led to a success. Now I can work from there and write the instance I actually need.

I'll close this now, but it would be great if errors would give a better error message though. Maybe I'll do a pull request later on.

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

No branches or pull requests

4 participants