Technical Details:
Keycloak Version: 12.0.2
Java Version: 1.8
Java Admin Client: 12.0.2
Keycloak Spring boot starter
Description
I have a Spring Boot application where internal staff can create and modify Keycloak users, using its official java dependency. This application should also contain an impersonation function. I tried impersonating with the Java Admin Client, the impersonate function returns a map, which contains the Boolean “sameRealm” and the redirect URL to the account page of Keycloak. With these 2 values I can’t access the session of the impersonated User nor can I really use these attributes for something else. Afterwards I tried the impersonate function of the Keycloak Rest API, the API returned some Cookies in the header unfortunately I can’t seem to figure out how to use these cookies, but I tried creating these cookies and set them but unfortunately it didn’t work. Lastly, I tried a token exchange to receive a valid access token and fortunately the function works.
token-exchange function
Keycloak keycloakService = KeycloakBuilder.builder()
.serverUrl(serverUrl)
.realm(realm)
.clientId(clientId)
.grantType(OAuth2Constants.CLIENT_CREDENTIALS)
.clientSecret(clientSecret)
.build();
BasicCookieStore cookieStore = new BasicCookieStore();
CloseableHttpClient httpClient = HttpClientBuilder.create().setDefaultCookieStore(cookieStore).build();
HttpUriRequest reqBuild = RequestBuilder.post()
.setUri(serverUrl + "/realms/intern/protocol/openid-connect/token")
.addHeader("Content-Type", "application/x-www-form-urlencoded")
.addParameter("client_id", "example")
.addParameter("client_secret", "example") //
.addParameter("grant_type", "urn:ietf:params:oauth:grant-type:token-exchange")
.addParameter("subject_token", keycloakService.tokenManager().getAccessTokenString())
.addParameter("requested_token_type", "urn:ietf:params:oauth:token-type:access_token")
.addParameter("requested_subject", userId)
.addParameter("audience", "target-client")
.build();
HttpResponse res = httpClient.execute(reqBuild);
String resBody = EntityUtils.toString(res.getEntity());
System.out.println(resBody);
With the above code I receive a valid access-token from Keycloak. But unfortunately, I don’t know how I can use this token to complete the Impersonation process.
The setup is as following: id.example.com provides the Spring Boot Application, sso.id.example.com runs KeyCloak instance. For my understanding, I should create a cookie for sso.id.example.com
If someone has another solution on how I can Impersonate a user and then acquire the associated session. I would appreciate the help.
Your resBody.content is of type AccessTokenResponse (assuming you got a 200 response) which has an token property. The token should be the auth_token for the user you want to impersonate. As a sanity check you can use https://jwt.io/ to debug the jwt token returned to make sure it is for the correct user.
You can use this token for subsequent calls as the person you are impersonating.
I don't believe you need to set a cookie store, i am certain you can remove this and it should continue to work.
import org.keycloak.admin.client.Keycloak
...
val keycloakService = KeycloakBuilder.builder()
.serverUrl(serverUrl)
.realm(realm)
.clientId(clientId)
.grantType(OAuth2Constants.CLIENT_CREDENTIALS)
.clientSecret(clientSecret)
.build();
val httpClient = HttpClientBuilder.create().build();
val reqBuild = RequestBuilder.post()
.setUri(serverUrl + "/realms/intern/protocol/openid-connect/token")
.addHeader("Content-Type", "application/x-www-form-urlencoded")
.addParameter("client_id", "example")
.addParameter("client_secret", "example") //
.addParameter("grant_type", "urn:ietf:params:oauth:grant-type:token-exchange")
.addParameter("subject_token", keycloakService.tokenManager().getAccessTokenString())
.addParameter("requested_token_type", "urn:ietf:params:oauth:token-type:access_token")
.addParameter("requested_subject", userId)
.addParameter("audience", "target-client")
.build();
val response = httpClient.execute(reqBuild)
val entity = if (response.statusLine.statusCode == 200) {
val mapper = ObjectMapper().registerModule(KotlinModule())
mapper.readValue(response.entity.content, AccessTokenResponse::class.java)
} else {
// handle error
}
// this should log you in as the impersonated user
val impersonated = Keycloak.getInstance(serverUrl, realm, clientId, entity.token).realm(realm)
Related
I am trying to build a client system which fetches the inbox of the user emails in outlook. The requirement is that the system should ask the user to login through Outlook once to authenticate and it should be able to fetch the emails and other actions enables via graph API like reply, forward etc there after without any login prompt. For this I tried using the MS Graph SDK and chose the authorization code flow for the same . I followed the following steps
Requested an authorization code by using the link : https://login.microsoftonline.com/{tenant}/oauth2/v2.0/authorize?client_id=clientId&response_type=code&redirect_uri=http%3A%2%2Flocalhost%2Fmyapp%2F&scope=https%3A%2F%2Fgraph.microsoft.com%2Fmail.read%20api%3A%2F%2F
I have stored this authorization code to the db and used the following snippet for authorization.
final AuthorizationCodeCredential authCodeCredential = new AuthorizationCodeCredentialBuilder()
.clientId("client id")
.clientSecret("client secret")
.authorizationCode("authcode from the db for that user")
.redirectUrl("http://localhost:8080/redirect/url")
.build();
final List<String> scopes = new ArrayList<String>();
scopes.add("Mail.Send");
scopes.add("offline_access");
scopes.add("openid");
final TokenCredentialAuthProvider tokenCredentialAuthProvider = new TokenCredentialAuthProvider(scopes, authCodeCredential);
GraphServiceClient graphClient = GraphServiceClient.builder().authenticationProvider( authProvider ).buildClient();
MessageCollectionPage messages = graphClient.me()
.messages()
.buildRequest()
.get();
This works the first time the fetched authorization code is fetched by user interaction , but when the same code is reused to avoid user interactions it throws the following
AADSTS54005: OAuth2 Authorization code was already redeemed, please retry with a new valid code or use an existing refresh token.
Does this mean that to make this flow work I have to request my users to login everytime for a single API call via graph client. Is there any way in which we can use the access code got in the AuthorizationCodeCredential and use it with GraphServiceClient to access microsoft Graph APIs using the SDK . Will hugely appreciate any kind of help on this. thank you
I'm new to Azure and I'm struggling to get a simple demo working.
I'm trying to write a daemon client that accesses a blob. The access must be via a registered app, and I want to explicitly acquire an access token.
As a warm-up I wrote a demo that accesses the blob using the blob api, and this works OK:
ClientCertificateCredential credentials = new ClientCertificateCredentialBuilder()
.clientId("11518eab-7b5a-493c-8d12-27731fe51341")
.tenantId("4b106bc5-7518-4f86-a259-f5726d124732")
.pemCertificate("/home/william/cert/new/azure.pem")
.build();
BlobServiceClient blobServiceClient = new BlobServiceClientBuilder()
.credential(credentials)
.endpoint("https://wemsa.blob.core.windows.net/")
.buildClient();
BlobContainerClient blobContainerClient = blobServiceClient.getBlobContainerClient("wemco");
blobContainerClient.getBlobClient("spiral.jpeg")
.downloadToFile("/home/william/blob.jpeg");
The above code uses my registered app to create a BlobClient that downloads my blob file. The fact that this works shows me that my app does have the role required to access my storage container.
I want to create a second demo that does the same thing, but I want to get an access token explicitly and use it to authorise a plain http request to the blob api. So far this is what I've produced:
private static final String TENANT_ID = "4b106bc5-7518-4f86-a259-f5726d124732";
private static final String CLIENT_ID = "11518eab-7b5a-493c-8d12-27731fe51341";
private static final String AUTHORITY = "https://login.microsoftonline.com/" + TENANT_ID;
private static final Set<String> SCOPE = Collections.singleton("api://" + CLIENT_ID + "/.default");
InputStream pkcs12Cert = new FileInputStream("/home/william/cert/new/azure.p12");
String certPwd = "password123";
IClientCredential credential = ClientCredentialFactory.createFromCertificate(pkcs12Cert, certPwd);
ConfidentialClientApplication cca = ConfidentialClientApplication
.builder(CLIENT_ID, credential).authority(AUTHORITY).build();
ClientCredentialParameters parameters = ClientCredentialParameters.builder(SCOPE).build();
IAuthenticationResult result = cca.acquireToken(parameters).join();
String token = result.accessToken();
URI uri = new URI("https://wemsa.blob.core.windows.net/wemco/spiral.jpeg");
HttpClient httpClient = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(uri)
.header("Authorization", "Bearer " + token)
.header("Date", formatNow())
.header("x-ms-version", "2020-06-12")
.GET()
.build();
HttpResponse<String> response = httpClient.send(request, BodyHandlers.ofString());
System.out.println(response.statusCode()); // 401 !!!
This uses the same registered app as the first demo. Debugging tells me that it is successfully getting access tokens. But, when I try to use them I always get a 401 response. I had hoped that since the first demo works OK, the second demo would automatically work as well - but it doesn't.
Is what I'm trying to do even possible?
Is there something wrong with the code in my second demo?
Do I need to do more setup in Azure before demo 2 will work? Some of the Azure docs mention creating an app role, which I tried, but I didn't know what to do with it.
Any suggestions ?
Additional info: This is what I did in Azure (accepting defaults for everything):
create storage account wemsa
create container wemco inside wemsa
upload (a small) file 'spiral.jpeg' into container wemco
create app registration wemapp
upload certificate 'azure.crt' into wemapp
Then I returned to the container wemco and:
granted role 'Storage Blob Data Contributor' to wemapp
This was enough to get demo 1 working.
Fixed by changing the scope (i.e. the requested resource) to azure storage:
private static final Set<String> SCOPE =
Collections.singleton("https://storage.azure.com//.default");
After making this change I was able to download my blob file using the demo 2 code in the original posting.
I am using authentication of users in Java 8 against Keycloak, with the Keycloak adapter API for Java.
In this case, the class KeycloakBuilder (keycloak-admin-client-6.0.0.jar) builds a Keycloak instance to perform authentication operations.
how can I request an offline token rather than a normal Bearer token using this API?
Have not found parameter or way to request it. I need tokens with 1 month expiration time, which cannot get unless change the "SSO Session Max" field, but I don´t want this to affect other Clients or users in the same Realm / client.
I am not sure if there are any specialties with the Keycloak Java adapter but I already implemented this with other clients. On the Authorization server side, you need to add a role offline_access to the users, which are allowed to request an offline session (this can be done explicitly or as a default role mapping). On the client side, you have to add another scope offline_access to the auth request. This can also be done by default (see default scopes). Please refer to the official Keycloak documentation about Offline Sessions for further details.
I post a possible solution using keycloak-authz-client library instead.
As stated by #Philipp , it is also necessary that the user you log in with has the role offline_access.
public String login(String username, String password) {
String authServerUrl = "http://localhost:18080/auth"; // Your keycloak auth entpoint
String realm = "realm"; // Realm
String clientId = "client"; // Client
Map<String, Object> clientCredentials = new LinkedHashMap<String, Object>();
clientCredentials.put("secret", "clientSecret"); // Client secret (Access Type: Confidential)
Configuration configuration = new Configuration(
authServerUrl,
realm,
clientId,
clientCredentials,
null
);
AuthzClient authzClient = AuthzClient.create(configuration);
AuthorizationRequest request = new AuthorizationRequest();
request.setScope("offline_access");
AuthorizationResponse response = authzClient.authorization(username, password).authorize(request);
return response.getRefreshToken(); // response.getToken() returns the bearer token
}
Currently, the mechanism that we use for authenticating against a server which requires OAuth2 is to write a Java program which contains a main() method, which runs an HttpClient to generate an OAuth2 access token by using this call:
https://api.externalsite.com/v1/oauth/token?clientId=iLHuXeULFBdW4B1dmRY0MhFILRQnlfeK&clientSecret=RG3JanXEq2R1GhRvIQ2d2AKRx0SORvb3&grant_type=client_credentials
This returns the following JSON payload:
{
"access_token": "eyJhbGciOi786I1NiJ9.eyJ1c2VybmFtZSI6bnVsbCwiZGV2aWNlSWQiOm51bGwsImNsaWVudElkIjoiaUxIdVhlVUxGQmRXNEIxZG1SWTBNaFJPTVJRbmxmZUsiLCJhZElkIjpudWxsLCJleHAiOjE1MjU0MjY4LMYsImlhdCI6MTUyNTQyMzE0Nn0.Zz_uhXqOF2ykC24mNBWHnQ_Vmx-jfQs3X4qcmmN0-Sk",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": null,
"scope": null
}
After obtaining the access token, we are able to run queries using JSON against the authorized website / service.
Question(s):
Inside a Spring Boot Microservice (2.0.1.RELEASE), how can one use Spring Security or just an HttpClient to use clientId, clientSecret and grant_type to automatically provide a global access token inside each REST call (which might be an HTTP Post) from the REST controller layer?
Can someone show a code sample of how to use Spring Security or a different library to just send the clientId, clientSecret, and grant_type to obtain an OAuth2 access token?
What to do (using the library from question # 2) if the OAuth2 token expires?
1) You dont need spring security .Just use 'io.jsonwebtoken.Jwts'.
You can use any number of parameters to generate the JWT token .
You can use a component inside your Spring boot application to generate the JWT token .
Then create a Token service that will use this bean and perform :generate access token , validate access token and refresh the token .
2)
Sample :
#Component
public String createJwtToken(User user, TokenType type, ClientKey clientKey) throws InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException {
String userName = user.getUsername();
Date currentTime = new Date();
String token = Jwts.builder()
.setSubject(userName)
.claim(Constants.NAME_KEY, Constants.NAME_VALUE)
.claim(Constants.USER_TOKEN_KEY, clientKey.getKey())
.claim(Constants.SCOPE_KEY, Constants.SCOPE_VALUE)
.claim(Constants.TOKEN_TYPE, type.name())
.setIssuer(tokenIssuer)
.setHeaderParam(Constants.TOKEN_TYP, Constants.TOKEN_JWT)
.setHeaderParam(Constants.TOKEN_TYPE, type.name())
.setIssuedAt(currentTime)
.setExpiration(timeout(type))
.signWith(SignatureAlgorithm.HS256, key)
.compact();
return encrypt(token);
}
3) Whenever you generate the token for the first time you generate 2 tokens : accessToken and Refresh Token .
AccessToken is short lived and expires soon . - say 5 mins .
The refresh token has a onger expiry duration : eg: 20 mins .
Purpose of refresh token is that you can use the refresh token to generate the new access token .
So when ur access token expires, just make a call to the refresh token method by passing ur refresh token . This method should return the user from redis with the new access token .
Regards,
R Rai
Found an OAuth2Client which open sourced and offered by IBM:
https://www.ibm.com/developerworks/library/se-oauthjavapt1/index.html#download
Also, just used RestTemplate:
String accessToken = OAuth2Client.generateAccessToken();
RestTemplate restTemplate = new RestTemplate();
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.set("Authorization", "Bearer "+accessToken);
HttpEntity<String> entity = new HttpEntity<String>(request,headers);
String response = restTemplate.postForObject(url, entity, String.class);
Very easy!
I want to create a user through keycloak admin client but I am getting a 400 status.
I cannot activate any logs of the keycloak client so I have no idea about the http call it performs.
I have managed to create a user through postman.
Search and get user work (see below)
Do you see any errors in my code?
Keycloak keycloak = Keycloak.getInstance(keycloakUrl,"master",
keycloakAdminUsername, keycloakAdminPassword,
keycloakAdminClient);
UserRepresentation user = new UserRepresentation();
user.setUsername("username");
user.setFirstName("firstname");
user.setEmail("kjhkjh#gmail.com");
user.setRequiredActions(Collections.<String>emptyList());
user.setEnabled(true);
Response response = keycloak.realm(realm).users().create(user);
if (response.getStatus() != 201) {
log.error("Couldn't create user, status:{}", response.getStatus());
} else {
log.info("User# created", user.getUsername());
log.info("Keycloak response status: {}",
response.getStatus());
}
Couldn't create user, status:400
The thing is, if I try to get a user using the same keycloak instance, it works.
UserResource userX = keycloak.realm(realm).users().get("8c1d29fd-8d6b-4dea-817f-3fc1421096f0");
log.debug(userX.toRepresentation().getUsername());
Same, if I use the search
log.debug("Getting users from keycloak");
List<UserRepresentation> users = keycloak.realm(realm).users().search(username, 0, 10);
users.forEach(user -> log.info("username: {}", user.getUsername()));
Well, I have found the solution, kind of stupid to my eyes. The client needs to be the same version with the keycloak server. In my case for example I got the 2.0.0.Final version of the keycloak admin client because the keycloak server is 2.0.0. It is all about the UserRepresentation class as far as I understood.