Using ADAL library in Java , I created token (using client credential provider) ,the resulted token is TokenCredentialAuthProvider which is of IAuthenticationProvider type. this token i was able to use to create a graph client.
But as i want to migrate from ADAL to MSAL ,I tried creating a token using MSAL4j (java) (using client credential provider) and the resulted token was of the form IAuthenticationResult , which i cant use to create Graph client as it requires an instance of IAuthenticationProvider.
how to create graph client using token of the form IAuthenticationResult (msal4j)
MS Graph clients creation require an instance of IAuthenticationProvider. Several built-in providers and samples are available in Choose a Microsoft Graph authentication provider based on scenario. If you already posses an token you can create your own class/implementation and return the token returned by IAuthenticationResult.getAccessToken() from within the getAuthorizationTokenAsync method.
E.g.
// MyAuthenticationProvider.java
package com.example;
import java.net.URL;
import java.util.concurrent.CompletableFuture;
import com.microsoft.graph.authentication.IAuthenticationProvider;
public class MyAuthenticationProvider implements IAuthenticationProvider {
private CompletableFuture<String> accessTokenFuture;
public MyAuthenticationProvider(String accessToken) {
this.accessTokenFuture = new CompletableFuture<>();
this.accessTokenFuture.complete(accessToken);
}
#Override
public CompletableFuture<String> getAuthorizationTokenAsync(URL requestUrl) {
return this.accessTokenFuture;
}
}
// App.java
package com.example;
import com.google.gson.Gson;
import com.microsoft.graph.authentication.IAuthenticationProvider;
import com.microsoft.graph.models.User;
import com.microsoft.graph.requests.GraphServiceClient;
public class App {
public static void main(String[] args) {
String msalAccessToken = args[0];
final IAuthenticationProvider auth = new MyAuthenticationProvider(msalAccessToken);
final GraphServiceClient graphClient = GraphServiceClient
.builder()
.authenticationProvider(auth)
.buildClient();
final User me = graphClient.me().buildRequest().get();
System.out.println(new Gson().toJson(me));
}
}
I am trying to upgrade to spring security 5.5.1 on a WebClient call.
I found out that the oauth2 clientId and secret are now URL encoded in AbstractWebClientReactiveOAuth2AccessTokenResponseClient, but my token provider does not support this (for example if the secret contains a + character it works only when it is sent as a + not as %2B).
I understand this is seen as a bug fix from spring-security side ), but I cannot make the token provider change its behavior easily.
So I tried to find a way to work around this.
The [documentation] (https://docs.spring.io/spring-security/site/docs/current/reference/html5/#customizing-the-access-token-request) on how to customize the access token request does not seem to apply when you use a WebClient configuration (which is my case).
In order to remove the clientid/secret encoding I had to extend and copy most of the existing code from AbstractWebClientReactiveOAuth2AccessTokenResponseClient to customize the WebClientReactiveClientCredentialsTokenResponseClient because most of it has private/default visibility.
I traced this in an enhancement issue in the spring-security project.
Is there an easier way to customize the Authorization header of the token request, in order to skip the url encoding ?
There is definitely room for improvement in some of the APIs around customization, and for sure these types of questions/requests/issues from the community will continue to help highlight those areas.
Regarding the AbstractWebClientReactiveOAuth2AccessTokenResponseClient in particular, there is currently no way to override the internal method to populate basic auth credentials in the Authorization header. However, you can customize the WebClient that is used to make the API call. If it's acceptable in your use case (temporarily, while the behavior change is being addressed and/or a customization option is added) you should be able to intercept the request in the WebClient.
Here's a configuration that will create a WebClient capable of using an OAuth2AuthorizedClient:
#Configuration
public class WebClientConfiguration {
#Bean
public WebClient webClient(ReactiveOAuth2AuthorizedClientManager authorizedClientManager) {
// #formatter:off
ServerOAuth2AuthorizedClientExchangeFilterFunction exchangeFilterFunction =
new ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
exchangeFilterFunction.setDefaultOAuth2AuthorizedClient(true);
return WebClient.builder()
.filter(exchangeFilterFunction)
.build();
// #formatter:on
}
#Bean
public ReactiveOAuth2AuthorizedClientManager authorizedClientManager(
ReactiveClientRegistrationRepository clientRegistrationRepository,
ServerOAuth2AuthorizedClientRepository authorizedClientRepository) {
// #formatter:off
WebClientReactiveClientCredentialsTokenResponseClient accessTokenResponseClient =
new WebClientReactiveClientCredentialsTokenResponseClient();
accessTokenResponseClient.setWebClient(createAccessTokenResponseWebClient());
ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider =
ReactiveOAuth2AuthorizedClientProviderBuilder.builder()
.clientCredentials(consumer ->
consumer.accessTokenResponseClient(accessTokenResponseClient)
.build())
.build();
DefaultReactiveOAuth2AuthorizedClientManager authorizedClientManager =
new DefaultReactiveOAuth2AuthorizedClientManager(
clientRegistrationRepository, authorizedClientRepository);
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
// #formatter:on
return authorizedClientManager;
}
protected WebClient createAccessTokenResponseWebClient() {
// #formatter:off
return WebClient.builder()
.filter((clientRequest, exchangeFunction) -> {
HttpHeaders headers = clientRequest.headers();
String authorizationHeader = headers.getFirst("Authorization");
Assert.notNull(authorizationHeader, "Authorization header cannot be null");
Assert.isTrue(authorizationHeader.startsWith("Basic "),
"Authorization header should start with Basic");
String encodedCredentials = authorizationHeader.substring("Basic ".length());
byte[] decodedBytes = Base64.getDecoder().decode(encodedCredentials);
String credentialsString = new String(decodedBytes, StandardCharsets.UTF_8);
Assert.isTrue(credentialsString.contains(":"), "Decoded credentials should contain a \":\"");
String[] credentials = credentialsString.split(":");
String clientId = URLDecoder.decode(credentials[0], StandardCharsets.UTF_8);
String clientSecret = URLDecoder.decode(credentials[1], StandardCharsets.UTF_8);
ClientRequest newClientRequest = ClientRequest.from(clientRequest)
.headers(httpHeaders -> httpHeaders.setBasicAuth(clientId, clientSecret))
.build();
return exchangeFunction.exchange(newClientRequest);
})
.build();
// #formatter:on
}
}
This test demonstrates that the credentials are decoded for the internal access token response WebClient:
#ExtendWith(MockitoExtension.class)
public class WebClientConfigurationTests {
private WebClientConfiguration webClientConfiguration;
#Mock
private ExchangeFunction exchangeFunction;
#Captor
private ArgumentCaptor<ClientRequest> clientRequestCaptor;
#BeforeEach
public void setUp() {
webClientConfiguration = new WebClientConfiguration();
}
#Test
public void exchangeWhenBasicAuthThenDecoded() {
WebClient webClient = webClientConfiguration.createAccessTokenResponseWebClient()
.mutate()
.exchangeFunction(exchangeFunction)
.build();
when(exchangeFunction.exchange(any(ClientRequest.class)))
.thenReturn(Mono.just(ClientResponse.create(HttpStatus.OK).build()));
webClient.post()
.uri("/oauth/token")
.headers(httpHeaders -> httpHeaders.setBasicAuth("aladdin", URLEncoder.encode("open sesame", StandardCharsets.UTF_8)))
.retrieve()
.bodyToMono(Void.class)
.block();
verify(exchangeFunction).exchange(clientRequestCaptor.capture());
ClientRequest clientRequest = clientRequestCaptor.getValue();
String authorizationHeader = clientRequest.headers().getFirst("Authorization");
assertThat(authorizationHeader).isNotNull();
String encodedCredentials = authorizationHeader.substring("Basic ".length());
byte[] decodedBytes = Base64.getDecoder().decode(encodedCredentials);
String credentialsString = new String(decodedBytes, StandardCharsets.UTF_8);
String[] credentials = credentialsString.split(":");
assertThat(credentials[0]).isEqualTo("aladdin");
assertThat(credentials[1]).isEqualTo("open sesame");
}
}
package com.example.GraphMS_API.service;
import java.util.Arrays;
import com.azure.identity.UsernamePasswordCredential; import
com.azure.identity.UsernamePasswordCredentialBuilder; import
com.microsoft.graph.authentication.TokenCredentialAuthProvider;
import com.microsoft.graph.models.User; import
com.microsoft.graph.requests.GraphServiceClient;
public class App {
public static void main(final String args[]) {
final UsernamePasswordCredential usernamePasswordCredential = new
UsernamePasswordCredentialBuilder()
.clientId("dfdfjhf").username("abc#xyz.com")
.password("1234").build();
final TokenCredentialAuthProvider tokenCredentialAuthProvider =
new TokenCredentialAuthProvider(
Arrays.asList("Mail.ReadWrite.Shared"), usernamePasswordCredential); final GraphServiceClient graphClient
= GraphServiceClient.builder()
.authenticationProvider(tokenCredentialAuthProvider).buildClient();
final User me = graphClient.me().buildRequest().get();
System.out.println(me.companyName);
} }
Have you tried isolating the issue from code and leveraging Postman/Graph Explorer to make same API call. Observe the behavior and see if you are able to obtain token.
Also make sure SDK you are trying to use is latest and updated.
Thanks!
I have a Keycloak protected backend that I would like to access via swagger-ui. Keycloak provides the oauth2 implicit and access code flow, but I was not able to make it work. Currently, Keycloak's documentation is lacking regarding which url should be used for authorizationUrl and tokenUrl within swagger.json.
Each realm within Keycloak offers a huge list of configuration urls by accessing http://keycloak.local/auth/realms/REALM/.well-known/openid-configuration
Furthermore I've tried to directly integrate the keycloak js-client within swagger-ui index.html by adding the following lines:
<script src="keycloak/keycloak.js"></script>
<script>
var keycloak = Keycloak('keycloak.json');
keycloak.init({ onLoad: 'login-required' })
.success(function (authenticated) {
console.log('Login Successful');
window.authorizations.add("oauth2", new ApiKeyAuthorization("Authorization", "Bearer " + keycloak.token, "header"));
}).error(function () {
console.error('Login Failed');
window.location.reload();
}
);
</script>
I also tried something like this after 'Login Successful'
swaggerUi.api.clientAuthorizations.add("key", new SwaggerClient.ApiKeyAuthorization("Authorization", "Bearer " + keycloak.token, "header"));
But it also doesn't work.
Any suggestions how I can integrate keycloak auth within swagger?
Swagger-ui can integrate with keycloak using the implicit authentication mode.
You can setup oauth2 on swagger-ui so that it will ask you to authenticate instead of giving swagger-ui the access token directly.
1st thing, your swagger need to reference a Security definition like:
"securityDefinitions": {
"oauth2": {
"type":"oauth2",
"authorizationUrl":"http://172.17.0.2:8080/auth/realms/master/protocol/openid-connect/auth",
"flow":"implicit",
"scopes": {
"openid":"openid",
"profile":"profile"
}
}
}
Then, you swagger-ui need to reference some other parameter: With the pure js, you can use in the index.html
const ui = SwaggerUIBundle({ ...} );
ui.initOAuth({
clientId: "test-uid",
realm: "Master",
appName: "swagger-ui",
scopeSeparator: " ",
additionalQueryStringParams: {"nonce": "132456"}
})
In this code,
authorizationUrl is the authorization endpoint on your keycloak realm
Scopes are something you can set to your needs
clientId is a client parametrized with implicit mode on keycloak realm
the additional parameter nonce should be random, but swagger-ui don't use it yet.
I add here an example if you want to do all this on Spring-boot:
On this framework, you will mainly use swagger and swagger-ui web-jar from Springfox. This is done by adding the dependencies:
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.8.0</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.8.0</version>
</dependency>
Swagger is enable by adding the annotation swagger2 on your main class:
#SpringBootApplication
#EnableSwagger2
public class TestSpringApplication {
...
then you can setup a Configuration class like this:
#Configuration
public class SwaggerConfigurer {
#Bean
public SecurityConfiguration securityConfiguration() {
Map<String, Object> additionalQueryStringParams=new HashMap<>();
additionalQueryStringParams.put("nonce","123456");
return SecurityConfigurationBuilder.builder()
.clientId("test-uid").realm("Master").appName("swagger-ui")
.additionalQueryStringParams(additionalQueryStringParams)
.build();
}
#Bean
public Docket api() {
return new Docket(DocumentationType.SWAGGER_2)
.select()
.apis(RequestHandlerSelectors.basePackage("com.example.testspring"))
.paths(PathSelectors.any())
.build().securitySchemes(buildSecurityScheme()).securityContexts(buildSecurityContext());
}
private List<SecurityContext> buildSecurityContext() {
List<SecurityReference> securityReferences = new ArrayList<>();
securityReferences.add(SecurityReference.builder().reference("oauth2").scopes(scopes().toArray(new AuthorizationScope[]{})).build());
SecurityContext context = SecurityContext.builder().forPaths(Predicates.alwaysTrue()).securityReferences(securityReferences).build();
List<SecurityContext> ret = new ArrayList<>();
ret.add(context);
return ret;
}
private List<? extends SecurityScheme> buildSecurityScheme() {
List<SecurityScheme> lst = new ArrayList<>();
// lst.add(new ApiKey("api_key", "X-API-KEY", "header"));
LoginEndpoint login = new LoginEndpointBuilder().url("http://172.17.0.2:8080/auth/realms/master/protocol/openid-connect/auth").build();
List<GrantType> gTypes = new ArrayList<>();
gTypes.add(new ImplicitGrant(login, "acces_token"));
lst.add(new OAuth("oauth2", scopes(), gTypes));
return lst;
}
private List<AuthorizationScope> scopes() {
List<AuthorizationScope> scopes = new ArrayList<>();
for (String scopeItem : new String[]{"openid=openid", "profile=profile"}) {
String scope[] = scopeItem.split("=");
if (scope.length == 2) {
scopes.add(new AuthorizationScopeBuilder().scope(scope[0]).description(scope[1]).build());
} else {
log.warn("Scope '{}' is not valid (format is scope=description)", scopeItem);
}
}
return scopes;
}
}
There is a lot of thing you can update in this code. This is mainly the same as before:
nonce which should be a random thing (swagger-ui don't use it yet)
clientId which you need to setup accordingly to the client you setup in keycloak
basePackage: You need to set the package in which all your controller are
If you need an api-key, you can enable it and add it on the security scheme list
LoginEndpoint: that need to be the authorization endpoint of you keycloak realm
scopeItems: the scopes you want for this authentication.
It will generate the same thing as before: Updating the swagger to add the securityDefinition and make swagger-UI take the parameter for clientId, nonce, ...
Was struggling with this setup for the past 2 days. Finally got a working solution for those who cannot resolve.
pom.xml
...
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-spring-security-adapter</artifactId>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-spring-boot-starter</artifactId>
</dependency>
...
Enable Swagger on main class
...
import springfox.documentation.swagger2.annotations.EnableSwagger2;
#SpringBootApplication
#EnableSwagger2
#EnableAsync
#EnableCaching
public class MainApplication {
public static void main(String[] args) {
SpringApplication app = new SpringApplication(MainApplication.class);
app.run(args);
}
}
SwaggerConfig.java
package com.XXX.XXXXXXXX.app.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.AuthorizationCodeGrantBuilder;
import springfox.documentation.builders.OAuthBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.service.*;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spi.service.contexts.SecurityContext;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger.web.SecurityConfiguration;
import springfox.documentation.swagger.web.SecurityConfigurationBuilder;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
import java.util.Arrays;
import static springfox.documentation.builders.PathSelectors.regex;
/*
* Setting up Swagger for spring boot
* https://www.baeldung.com/swagger-2-documentation-for-spring-rest-api
*/
#Configuration
#EnableSwagger2
public class SwaggerConfig {
#Value("${keycloak.auth-server-url}")
private String AUTH_SERVER;
#Value("${keycloak.credentials.secret}")
private String CLIENT_SECRET;
#Value("${keycloak.resource}")
private String CLIENT_ID;
#Value("${keycloak.realm}")
private String REALM;
private static final String OAUTH_NAME = "spring_oauth";
private static final String ALLOWED_PATHS = "/directory_to_controllers/.*";
private static final String GROUP_NAME = "XXXXXXX-api";
private static final String TITLE = "API Documentation for XXXXXXX Application";
private static final String DESCRIPTION = "Description here";
private static final String VERSION = "1.0";
#Bean
public Docket taskApi() {
return new Docket(DocumentationType.SWAGGER_2)
.groupName(GROUP_NAME)
.useDefaultResponseMessages(true)
.apiInfo(apiInfo())
.select()
.paths(regex(ALLOWED_PATHS))
.build()
.securitySchemes(Arrays.asList(securityScheme()))
.securityContexts(Arrays.asList(securityContext()));
}
private ApiInfo apiInfo() {
return new
ApiInfoBuilder().title(TITLE).description(DESCRIPTION).version(VERSION).build();
}
#Bean
public SecurityConfiguration security() {
return SecurityConfigurationBuilder.builder()
.realm(REALM)
.clientId(CLIENT_ID)
.clientSecret(CLIENT_SECRET)
.appName(GROUP_NAME)
.scopeSeparator(" ")
.build();
}
private SecurityScheme securityScheme() {
GrantType grantType =
new AuthorizationCodeGrantBuilder()
.tokenEndpoint(new TokenEndpoint(AUTH_SERVER + "/realms/" + REALM + "/protocol/openid-connect/token", GROUP_NAME))
.tokenRequestEndpoint(
new TokenRequestEndpoint(AUTH_SERVER + "/realms/" + REALM + "/protocol/openid-connect/auth", CLIENT_ID, CLIENT_SECRET))
.build();
SecurityScheme oauth =
new OAuthBuilder()
.name(OAUTH_NAME)
.grantTypes(Arrays.asList(grantType))
.scopes(Arrays.asList(scopes()))
.build();
return oauth;
}
private AuthorizationScope[] scopes() {
AuthorizationScope[] scopes = {
new AuthorizationScope("user", "for CRUD operations"),
new AuthorizationScope("read", "for read operations"),
new AuthorizationScope("write", "for write operations")
};
return scopes;
}
private SecurityContext securityContext() {
return SecurityContext.builder()
.securityReferences(Arrays.asList(new SecurityReference(OAUTH_NAME, scopes())))
.forPaths(PathSelectors.regex(ALLOWED_PATHS))
.build();
}
}
From terminal, run "mvnw spring-boot:run"
Open browser and hit http://localhost:[port]/[app_name]/swagger-ui.html.
Click the Authorize button:
Swagger Authorize Button
This should present a modal to confirm your keycloak settings.
Click Authorize button once again. You should be redirected to a login screen.
Once credentials are entered and confirmed, you will be redirected back to Swagger-UI fully authenticated.
Swagger-ui + Keycloak (or any other OAuth2 provider) using implicit flow, OpenAPI 3.0 template:
components:
...
securitySchemes:
my_auth_whatever:
type: oauth2
flows:
implicit:
authorizationUrl: https://MY-KEYCLOAK-HOST/auth/realms/MY-REALM-ID/protocol/openid-connect/auth
scopes: {}
...
security:
- my_auth_whatever: []
Make sure the implicit flow is enabled in Keycloak settings for the client that you use.
One downside is that the user is still asked for client_id in the modal when clicks on "Authorize" button in Swagger UI.
The value that user enters may be overwritten by adding query param ?client_id=YOUR-CLIENT-ID to the authorizationUrl but it's kinda the dirty hack and the modal is still showed to the user.
When running swagger-ui in docker - the OAUTH_CLIENT_ID env var may be provided to container to set the default client_id value for the modal.
For non-docker deployment refer to #wargre's approach with changing the index.html (not sure if there's a better way).
For SwaggerAPI (OpenAPI 2.0) example refer to first code snippet in #wargre's answer and this doc: https://swagger.io/docs/specification/2-0/authentication/
I'm trying to pull a timeline from my own twitter account in to a website using Scribe. I tried the provided Twitter example, but I'm always getting the following response from twitter:
{"errors":[{"message":"Could not authenticate you","code":32}]}
I tried this by using my own access token as well as a dynamically created one.
Code for using my own access token:
import org.scribe.builder.ServiceBuilder;
import org.scribe.builder.api.TwitterApi;
import org.scribe.model.OAuthRequest;
import org.scribe.model.Response;
import org.scribe.model.Token;
import org.scribe.model.Verb;
import org.scribe.oauth.OAuthService;
public class TwitterTest {
public static void main(String[] args) {
OAuthService service = new ServiceBuilder()
.provider(TwitterApi.SSL.class)
.apiKey("myApiKey")
.apiSecret("myApiSecret")
.build();
Token accessToken = new Token("myAccessToken", "myAccessTokenSecret");
OAuthRequest request = new OAuthRequest(Verb.GET, "https://api.twitter.com/1.1/statuses/user_timeline.json");
service.signRequest(accessToken, request);
Response response = request.send();
System.out.println(response.getBody());
}
}