简体   繁体   中英

How do I generate short lived service account access tokens in Keycloak

I am generating an access token for a service account as described in the Keycloak docs. As an example:

curl --location --request POST 'http://my.domain/auth/realms/aRealm/protocol/openid-connect/token' \
--header 'Authorization: Basic YXBcnZlcjpNlpuMwRWk1Y02ZFRE15vVp2YUdHJibmNalVvMFmxzVNQVg==' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=client_credentials' 

I do get a token back, but it expires in 30 mins, just as I have configured them in my realm settings, for normal users usage.

But for the use as a service account, I do not want such a long lived token. Even 10-15 secs should be enough to generate the token and then immediately make a request to the server.

I have seen a similar feature in Google auth docs ( https://cloud.google.com/iam/docs/create-short-lived-credentials-direct#rest_2 ) by passing a lifetime param, where you ask for tokens with a specific lifetime.

Keycloak docs ( https://www.keycloak.org/docs/latest/server_admin/#_service_accounts ) do not mention anything about shorter living tokens. Have I missed a section that mentions something like that? Or is there an alternative way to create short lived tokens?

EDIT:

The Keycloak client created is used by users to logging to the system. For that usecase, I want their token to be valid for 30mins before they have to be refreshed again. But for service accounts usage (when I want my distributed code to make an API request to my own servers) I want these tokens to have a very short lived lifetime (enough to just make the API call).

You can change a "access.token.lifespan" for specific client by PUT HTTP call.

PUT call http://my.domain/auth/realms/aRealm/clients/client-id

The Bearer token needs to get by admin with realms-manage role (or master admin, manage-clients )

在此处输入图像描述 This is example my.domain : http://localhost:8180

aRealm : test

client id : a6654164-d2f5-451d-8f8c-25f00f29eec5

"access.token.lifespan": 15

curl --location --request PUT 'http://localhost:8180/auth/admin/realms/test/clients/a6654164-d2f5-451d-8f8c-25f00f29eec5' \
--header 'Accept: application/json, text/plain, */*' \
--header 'Accept-Encoding: gzip, deflate, br' \
--header 'Accept-Language: en-US,en;q=0.9' \
--header 'Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJSQjNmSzRrSC1EWmxjLWQ0ZjB3b3JJM3c5d0ZEY0RidFZTS0xmTlJlcVVRIn0.eyJleHAiOjE2NTgwMDEzNjEsImlhdCI6MTY1Nzk2NTM2MSwianRpIjoiNGU4NzRhZWMtMGVjZC00MjEyLThiYmUtNGUxODM2OTRlMGM5IiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MTgwL2F1dGgvcmVhbG1zL21hc3RlciIsInN1YiI6ImFiOWNkMjc3LWFhYWEtNGI1My1iMzdlLTBhNzJiODZmZWI0OSIsInR5cCI6IkJlYXJlciIsImF6cCI6ImFkbWluLWNsaSIsInNlc3Npb25fc3RhdGUiOiI2YjU5MTNlMC1lOGFkLTQzM2MtOTkyOC1lNTBjMjQ0MGNmN2MiLCJhY3IiOiIxIiwic2NvcGUiOiJlbWFpbCBwcm9maWxlIiwic2lkIjoiNmI1OTEzZTAtZThhZC00MzNjLTk5MjgtZTUwYzI0NDBjZjdjIiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJhZG1pbiJ9.LhOegp1UbzaWOrNWb5aKhIzatIvNNg0JPbZeKqnumJ2pdZKL3xTei2Uo6GPKg_YRp9G-YAuCVrQFNWbhP1fDBrDbAEvZHT0Ho4OmlysISFIGP3i9Hr1x3uILLPxGks0iiP7RzCiSubtIwNZRl3nro5bRXkEx24F1drD4hWKW95Z9VAFPkSUW3Urk5Hdgm991pUwUdQCPiyyNj7RL2uiHEsSEoypoT2CviZ518dElmnNFmPafg5K_j39atHX5DxwxEvT5cTfgD6Sg3CmrJupE3CfY31N8OfkmBCA__3mOx31btncK4uG9EsYujxSeHxEZhPV0gUwCx7ZykYkfKhl0OQ' \
--header 'Content-Type: application/json' \
--data-raw '{
  "id": "a6654164-d2f5-451d-8f8c-25f00f29eec5",
  "clientId": "my-test-client",
  "surrogateAuthRequired": false,
  "enabled": true,
  "alwaysDisplayInConsole": false,
  "clientAuthenticatorType": "client-secret",
  "redirectUris": [
    "http://localhost:8180/test/*"
  ],
  "webOrigins": [],
  "notBefore": 0,
  "bearerOnly": false,
  "consentRequired": false,
  "standardFlowEnabled": true,
  "implicitFlowEnabled": false,
  "directAccessGrantsEnabled": true,
  "serviceAccountsEnabled": true,
  "authorizationServicesEnabled": true,
  "publicClient": false,
  "frontchannelLogout": false,
  "protocol": "openid-connect",
  "attributes": {
    "access.token.lifespan": 15,
    "saml.multivalued.roles": "false",
    "saml.force.post.binding": "false",
    "frontchannel.logout.session.required": "false",
    "oauth2.device.authorization.grant.enabled": "false",
    "backchannel.logout.revoke.offline.tokens": "false",
    "saml.server.signature.keyinfo.ext": "false",
    "use.refresh.tokens": "true",
    "oidc.ciba.grant.enabled": "false",
    "backchannel.logout.session.required": "true",
    "client_credentials.use_refresh_token": "false",
    "saml.client.signature": "false",
    "require.pushed.authorization.requests": "false",
    "saml.allow.ecp.flow": "false",
    "saml.assertion.signature": "false",
    "id.token.as.detached.signature": "false",
    "client.secret.creation.time": "1657583810",
    "saml.encrypt": "false",
    "saml.server.signature": "false",
    "exclude.session.state.from.auth.response": "false",
    "saml.artifact.binding": "false",
    "saml_force_name_id_format": "false",
    "tls.client.certificate.bound.access.tokens": "false",
    "acr.loa.map": "{}",
    "saml.authnstatement": "false",
    "display.on.consent.screen": "false",
    "token.response.type.bearer.lower-case": "false",
    "saml.onetimeuse.condition": "false",
    "request.uris": null,
    "frontchannel.logout.url": null,
    "default.acr.values": null,
    "oauth2.device.polling.interval": null
  },
  "authenticationFlowBindingOverrides": {},
  "fullScopeAllowed": true,
  "nodeReRegistrationTimeout": -1,
  "protocolMappers": [
    {
      "id": "fc55831d-9f85-4db5-8340-38e246fcfdf0",
      "name": "Client ID",
      "protocol": "openid-connect",
      "protocolMapper": "oidc-usersessionmodel-note-mapper",
      "consentRequired": false,
      "config": {
        "user.session.note": "clientId",
        "id.token.claim": "true",
        "access.token.claim": "true",
        "claim.name": "clientId",
        "jsonType.label": "String"
      }
    },
    {
      "id": "8e7e2a2c-90be-41a0-8a75-87099184a4a4",
      "name": "Client IP Address",
      "protocol": "openid-connect",
      "protocolMapper": "oidc-usersessionmodel-note-mapper",
      "consentRequired": false,
      "config": {
        "user.session.note": "clientAddress",
        "id.token.claim": "true",
        "access.token.claim": "true",
        "claim.name": "clientAddress",
        "jsonType.label": "String"
      }
    },
    {
      "id": "bee09b85-a3db-471a-aa51-9f1504df0a6f",
      "name": "Client Host",
      "protocol": "openid-connect",
      "protocolMapper": "oidc-usersessionmodel-note-mapper",
      "consentRequired": false,
      "config": {
        "user.session.note": "clientHost",
        "id.token.claim": "true",
        "access.token.claim": "true",
        "claim.name": "clientHost",
        "jsonType.label": "String"
      }
    }
  ],
  "defaultClientScopes": [
    "web-origins",
    "acr",
    "roles",
    "profile",
    "email"
  ],
  "optionalClientScopes": [
    "address",
    "phone",
    "offline_access",
    "microprofile-jwt"
  ],
  "access": {
    "view": true,
    "configure": true,
    "manage": true
  }
}'

you can check it's value by GET HTTP call The "expires_in":15 tell how much the lifetime

In here, that value is 15 seconds. It should be match as I sent by PUT call.

  curl --location --request GET 'http://localhost:8180/auth/admin/realms/test/clients/a6654164-d2f5-451d-8f8c-25f00f29eec5' \
> --header 'Accept: application/json, text/plain, */*' \
> --header 'Accept-Encoding: gzip, deflate, br' \
> --header 'Accept-Language: en-US,en;q=0.9' \
> --header 'Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJMWkdvVDZ6WFdtMU5BNmh2WXhlbUFrVmFaQWJodnlmWlo4Q2JqRWJ0U3RrIn0.eyJleHAiOjE2NTc5Nzg4MTMsImlhdCI6MTY1Nzk2ODAxMywianRpIjoiOGQ5YjhmYTYtYTk3OS00NGE1LWJhN2YtYTg2M2Q1NjU3NGIwIiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MTgwL2F1dGgvcmVhbG1zL3Rlc3QiLCJzdWIiOiI1MzkxMDBmMS01ZmEzLTQxMTUtYTZlYi0zOGNhY2NjM2E3NGEiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJhZG1pbi1jbGkiLCJzZXNzaW9uX3N0YXRlIjoiNGRmNjQzMDgtYzg5NC00NGYxLTk4YjctYzZmNDg3YTYzYWYzIiwiYWNyIjoiMSIsInNjb3BlIjoiZW1haWwgcHJvZmlsZSIsInNpZCI6IjRkZjY0MzA4LWM4OTQtNDRmMS05OGI3LWM2ZjQ4N2E2M2FmMyIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwibmFtZSI6ImZpcnN0IGxhc3QiLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJ1c2VyMSIsImdpdmVuX25hbWUiOiJmaXJzdCIsImZhbWlseV9uYW1lIjoibGFzdCIsImVtYWlsIjoidXNlcjFAdGVzdC5jb20ifQ.hjWKdTzSTjJpfW2hRqDOTML_5Uo2s5glAEDzVE567huW0LdYNtElvoi_wdLXNsYwCcVgW8juHsRHdoTlljT91zoZeY9VFg1YUJCF6k6uEkZXgSKvhm87jiyPMQa1Ex_b7wmOza0SFrhz3--PVMKgV6EJ2R1GtbwXfwQeLxsKslaIvjgAIliHNlIkxOA9bTnyFwOpvv93km1E9S3KjExcN1jq-KptslSa8lY35ExyCIatWbu_g9nrWlcQGg14qM8VfcJhrmL4ZDb0uaBk8HbZ52RNU8qu2pB584TsZ5iR29afRF5jTY-3pRiKUnTTOnQDrKLkqPvLhVwJPiRABcClDw'
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  2910  100  2910    0     0   265k      0 --:--:-- --:--:-- --:--:--  284k{"id":"a6654164-d2f5-451d-8f8c-25f00f29eec5","clientId":"my-test-client","surrogateAuthRequired":false,"enabled":true,"alwaysDisplayInConsole":false,"clientAuthenticatorType":"client-secret","redirectUris":["http://localhost:8180/test/*"],"webOrigins":[],"notBefore":0,"bearerOnly":false,"consentRequired":false,"standardFlowEnabled":true,"implicitFlowEnabled":false,"directAccessGrantsEnabled":true,"serviceAccountsEnabled":true,"authorizationServicesEnabled":true,"publicClient":false,"frontchannelLogout":false,"protocol":"openid-connect","attributes":{"access.token.lifespan":"15","saml.multivalued.roles":"false","saml.force.post.binding":"false","frontchannel.logout.session.required":"false","oauth2.device.authorization.grant.enabled":"false","backchannel.logout.revoke.offline.tokens":"false","saml.server.signature.keyinfo.ext":"false","use.refresh.tokens":"true","oidc.ciba.grant.enabled":"false","backchannel.logout.session.required":"true","client_credentials.use_refresh_token":"false","saml.client.signature":"false","require.pushed.authorization.requests":"false","saml.allow.ecp.flow":"false","saml.assertion.signature":"false","id.token.as.detached.signature":"false","client.secret.creation.time":"1657583810","saml.encrypt":"false","saml.server.signature":"false","exclude.session.state.from.auth.response":"false","saml.artifact.binding":"false","saml_force_name_id_format":"false","tls.client.certificate.bound.access.tokens":"false","acr.loa.map":"{}","saml.authnstatement":"false","display.on.consent.screen":"false","token.response.type.bearer.lower-case":"false","saml.onetimeuse.condition":"false"},"authenticationFlowBindingOverrides":{},"fullScopeAllowed":true,"nodeReRegistrationTimeout":-1,"protocolMappers":[{"id":"fc55831d-9f85-4db5-8340-38e246fcfdf0","name":"Client ID","protocol":"openid-connect","protocolMapper":"oidc-usersessionmodel-note-mapper","consentRequired":false,"config":{"user.session.note":"clientId","id.token.claim":"true","access.token.claim":"true","claim.name":"clientId","jsonType.label":"String"}},{"id":"8e7e2a2c-90be-41a0-8a75-87099184a4a4","name":"Client IP Address","protocol":"openid-connect","protocolMapper":"oidc-usersessionmodel-note-mapper","consentRequired":false,"config":{"user.session.note":"clientAddress","id.token.claim":"true","access.token.claim":"true","claim.name":"clientAddress","jsonType.label":"String"}},{"id":"bee09b85-a3db-471a-aa51-9f1504df0a6f","name":"Client Host","protocol":"openid-connect","protocolMapper":"oidc-usersessionmodel-note-mapper","consentRequired":false,"config":{"user.session.note":"clientHost","id.token.claim":"true","access.token.claim":"true","claim.name":"clientHost","jsonType.label":"String"}}],"defaultClientScopes":["web-origins","acr","roles","profile","email"],"optionalClientScopes":["address","phone","offline_access","microprofile-jwt"],"access":{"view":true,"configure":true,"manage":true}}

Looking at your requirements, I am under the impression that you are mixing concerns and different authentication flows.

The Keycloak client created is used by users to logging to the system. For that use-case, I want their token to be valid for 30mins before they have to be refreshed again.

Without knowing more details about your application, this use-case is typically solved by using a public client . That client would typically use Standard flow, Implicit Flow or Direct Access Grants enabled (not recommended). If possible preferably using Standard flow. Those flows can be used with a confidential client as well -- as long as the secret is stored safely in the backend -- however, it is not very natural , and unnecessary.

The Service Accounts flow ( ie , client credentials in OAuth2 terminology) forces the use of a confidential client . Hence, the use of the client_secret parameter in the request.

So there we start to see conflicts between best practices and the requirements that you have stated.

IMO the cleanest solution is to have a public client that is used by your users to authenticate normally, using the appropriate flow for that. In that client, you would set the 30 minutes as the token lifespan. And for:

But for service accounts usage (when I want my distributed code to make an API request to my own servers) I want these tokens to have a very short lived lifetime (enough to just make the API call).

For that use-case, use a different client, a confidential one , with the Service Account flow enabled. Go to the client Advanced Settings menu and set the Access Token Lifespan to 1 minute for example. This value will override the value set at the Realm Level (actually, in practice in can be more complicated than that ). The smallest time granularity that you can choose is minutes, for seconds you would have to use the approach showcased by @Bench Vue .

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM