I am currently in the process of connecting a Spring Boot application with Microsoft Dataverse.
The following requirements exist
CRUD operations in Dataverse tables
Users to log in to the Spring application using OAuth2 via the Microsoft account.
Attached is a sequence diagram to illustrate this
Sequence diagram
Unfortunately, I am failing with a 401 Unauthorized on the final access to a dataverse table.
I have done the following so far
entered the web application in Azure "App-Registration".
App Registration
generated client ID and client secret in Azure "App-Registration
Client Secrect & ID
set the API Rights to Dynamics CRM and Microsoft Graph in Azure "App-Registration".
API Access rights
Entered the app user in the Power Apps Admin area and gave access to the corresponding table via the role.
App User
Furthermore I have created the following code
dependencies in Maven
<dependency>
<groupId>com.azure.spring</groupId>
<artifactId>spring-cloud-azure-starter-active-directory</artifactId>
</dependency>
<dependency>
<groupId>com.azure.spring</groupId>
<artifactId>azure-spring-boot-starter-active-directory</artifactId>
<version>4.0.0</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-reactive-httpclient</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-client</artifactId>
</dependency>
<dependency>
application.yaml
spring.cloud.azure.active-directory:
enabled: true
profile.tenant-id: xxxx-xxxx-xxxx-xxxx-xxxx
credential:
client-id: xxxxxx-xxxx-xxxx-xxxx-xxxxx
client-secret: xxxxxxxxxxxxxxxxxxx
application-type: web-application
authorization-clients:
graph:
authorizationGrantType: client-credentials
scopes:
https://graph.microsoft.com/User.Read
dynamics:
authorizationGrantType: client-credentials
scopes:
https://xxxxxxx.crm4.dynamics.com/.default
rest service
import static org.springframework.security.oauth2.client.web.reactive.function.client.ServletOAuth2AuthorizedClientExchangeFilterFunction.oauth2AuthorizedClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.security.oauth2.client.AuthorizedClientServiceOAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;
import org.springframework.security.oauth2.client.annotation.RegisteredOAuth2AuthorizedClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.function.client.WebClient.RequestHeadersUriSpec;
import org.springframework.web.reactive.function.client.WebClient.ResponseSpec;
import reactor.netty.http.client.HttpClient;
import reactor.netty.transport.logging.AdvancedByteBufFormat;
#RestController
public class DataverseRestController {
#Autowired
private WebClient webClient;
#Autowired
private OAuth2AuthorizedClientService manager;
#GetMapping("/graph")
#ResponseBody
public String produkte(#RegisteredOAuth2AuthorizedClient OAuth2AuthorizedClient client) {
if (null == client) {
return "Get response failed.";
}
RequestHeadersUriSpec<?> requestHeadersUriSpec = webClient.get();
ResponseSpec response = requestHeadersUriSpec
.uri("https://xxxxxxxx.crm4.dynamics.com/api/data/v9.2/cr2a0_xxxxxxxxx")
.attributes(oauth2AuthorizedClient(client)).retrieve();
String body = response.bodyToMono(String.class).block();
return "Get response " + (null != body ? "successfully" : "failed");
}
}
webclient config
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.web.reactive.function.client.ServletOAuth2AuthorizedClientExchangeFilterFunction;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.netty.http.client.HttpClient;
import reactor.netty.transport.logging.AdvancedByteBufFormat;
#Configuration
public class WebClientConfig {
#Bean
public WebClient webClient(OAuth2AuthorizedClientManager oAuth2AuthorizedClientManager) {
ServletOAuth2AuthorizedClientExchangeFilterFunction function = new ServletOAuth2AuthorizedClientExchangeFilterFunction(
oAuth2AuthorizedClientManager);
HttpClient httpClient = HttpClient.create().wiretap("reactor.netty.http.client.HttpClient",
io.netty.handler.logging.LogLevel.DEBUG, AdvancedByteBufFormat.TEXTUAL);
return WebClient.builder().clientConnector(new ReactorClientHttpConnector(httpClient)).apply(function.oauth2Configuration()).build();
}
}
I could make the following observations
it is strange, if I log in with my standard user, which has access to the table, I cannot call the table via the Rest API (401). If I log in normally as an admin, I can't either (401). However, if I go to the Power Platform Admin Center in the environment and call up the environment URL there via the link, I have to log in again even though I am already authenticated as an admin. Afterwards I can also call up the data of the table in Dataverse from the browser with the same URL. Link where I logged in an then the tables are shown
when calling the rest interface I can't specify the authorised client which is configured in the application.yaml. #RegisteredOAuth2AuthorizedClient and #RegisteredOAuth2AuthorizedClient("azure") work. #RegisteredOAuth2AuthorizedClient("dynamics") does not. Here comes null
the web application correctly forwards the unauthenticated user to Microsoft and the necessary data is also forwarded to the application as a token.
Can someone here possibly help and clarify what I am doing wrong?
My guess is that the Application.yaml file is not resolved correctly in the configuration and the scope for access to Dynamics is not sent in the OAuth2 handshake. This means that the user is not authorized for the Dynamics client. This would also explain that the authorized client is not present. However, I do not know what is wrong in the config
After a long research and trying, I have now come to the solution.
spring.cloud.azure.active-directory:
enabled: true
profile.tenant-id: xxxx-xxxx-xxxx-xxxx-xxxx
credential:
client-id: xxxxxx-xxxx-xxxx-xxxx-xxxxx
client-secret: xxxxxxxxxxxxxxxxxxx
application-type: web-application
authorization-clients:
**azure**:
authorizationGrantType: **authorization_code**
scopes:
https://xxxxxxx.crm4.dynamics.com/.default
is the right solution for the configuration
Related
Unable to logout from google account after logging out through web app (Using google authorization for log into my app). When I hit the login URL next time for fresh login, instead of loading the sign in page, my application signed in with the last user credentials.
When I check various SO Q & A, I found out that I need to revoke the authorization token from token store. Below is the code I have tried:
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
class LoginController
{
#PostMapping("/logout")
public String logout_invoked(Authentication authentication)
{
Optional.ofNullable(authentication).ifPresent(auth -> {
OAuth2AccessToken accessToken = tokenStore.getAccessToken((OAuth2Authentication) auth); // classCast exception unable to convert Authentication to OAuth2Authentication
Optional.ofNullable(accessToken).ifPresent(oAuth2AccessToken -> {
Optional.ofNullable(oAuth2AccessToken.getRefreshToken()).ifPresent(tokenStore::removeRefreshToken);
tokenStore.removeAccessToken(accessToken);
});
});
return "logoutPage";
}
}
[Request processing failed; nested exception is
java.lang.ClassCastException: class
org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken
cannot be cast to class
org.springframework.security.oauth2.provider.OAuth2Authentication
(org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken
and org.springframework.security.oauth2.provider.OAuth2Authentication
are in unnamed module of loader 'app')] with root cause
I need to remove authorization token specific to the user who logging out currently.
P.S
As a work around, test user needs to sign out from web application followed by google account from browser to avoid same user signing in back.
I have a very simple endpoint
#PostMapping("/exception")
public String exception() {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST);
}
in 2 different machines. On the first machine this code is in a very simple spring boot app and it works as it is supposed to be working - when invoked, it returns 400 BAD_REQUEST. On the second machine, I have real spring boot project, with a lot of stuff. There, instead of having BAD_REQUEST returned, i get 405 MethodNotAllowed.
I don't even know what can be causing this behavior. Do you have any idea what is the case?
I am attaching a screenshot of the postman request that I use.
Postman screenshot
The whole controller:
package com.xxx.service.max.web.controller;
import com.xxx.service.max.model.context.UserContext;
import com.xxx.service.max.services.cas.CustomerAccountService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.server.ResponseStatusException;
import static com.xxx.service.max.constant.Constants.MY_ACCOUNT_X_REST;
#RestController
#RequestMapping(MY_ACCOUNT_X_REST)
public class ChangeLocaleController {
private static final Logger LOG = LoggerFactory.getLogger(ChangeLocaleController.class);
private UserContext userContext;
private CustomerAccountService customerAccountService;
#PostMapping("/exception")
public String exception() {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST);
}
#Autowired
public void setUserContext(UserContext userContext) {
this.userContext = userContext;
}
#Autowired
public void setCustomerAccountService(CustomerAccountService customerAccountService) {
this.customerAccountService = customerAccountService;
}
}
Make sure you are sending a POST request.
The 405 Method Not Allowed error occurs when the web server is configured in a way that does not allow you to perform a specific action for a particular URL. It's an HTTP response status code that indicates that the request method is known by the server but is not supported by the target resource.
Source
If you are simply entering the URL in your browser that is a GET request and you would get a 405.
I am using latest Grails and the spring security plugin. I would like to log in a predefined guest user at application start up but not sure how to achieve this.
How do I programmatically log in a user? (I'm attempting this in bootstrap but can not find what to import for the AuthToken class)
Where is this best done - i.e. in the bootstrap config?
Okay I found a solution... it's a bit raw, and I'm not sure this is the best place for it at application startup, but it achieves what I wanted.
So in Bootstrap file I have implemented the following:
Here's my imports:-
import grails.plugin.springsecurity.rest.token.AccessToken
import grails.plugin.springsecurity.rest.token.generation.TokenGenerator
import org.springframework.security.authentication.AuthenticationManager
import org.springframework.security.authentication.BadCredentialsException
import org.springframework.security.core.Authentication
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
import org.springframework.security.core.userdetails.UserDetails
...after all bootstrapping of my user I want to auto log in a Guest user and then generate the JWT refresh and access token too...
/* Login in guest user on application startup */
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("guest", "guest");
token.setDetails(tttGuest);
try {
//doing actual authentication
Authentication auth = authenticationManager.authenticate(token);
log.debug("Login succeeded!");
//setting principal in context
SecurityContextHolder.getContext().setAuthentication(auth);
//Generate JWT access token and refresh token
AccessToken accessToken = tokenGenerator.generateAccessToken(springSecurityService.principal as UserDetails)
return true
} catch (BadCredentialsException e) {
log.debug("Login failed")
return false
}
The only part left to do is to figure out how to communicated the tokens back to the client application.
I want try do a Post request from my frontend (Angular 2) to my backend (Spring). But I can't.
The error:
GET http://localhost:8080/loginPost 405 ()
Failed to load http://localhost:8080/loginPost: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://192.168.0.190:4200' is therefore not allowed access. The response had HTTP status code 405.
My Angular Service:
import {Injectable} from "#angular/core";
import { Headers, Http } from '#angular/http';
import 'rxjs/add/operator/toPromise';
import { Login } from'../data-login/models/login.model';
#Injectable()
export class LoginService{
private loginUrl = 'http://localhost:8080/loginPost'; // URL to web API
private headers = new Headers({'Content-Type': 'application/json'});
constructor(private http: Http){}
loginQuery(login: Login){
console.log(login.id);
return this.http.request(this.loginUrl,JSON.stringify(login));
}
}
My Spring Backend Code:
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
#RestController
public class LoginProvider {
#CrossOrigin(origins = "http://192.168.0.190:4200")
#PostMapping(value="/loginPost", consumes = { MediaType.APPLICATION_JSON_UTF8_VALUE })
public ResponseEntity<?> verifyLogin(#RequestBody String maoe){
System.out.println(maoe);
return new ResponseEntity<>(HttpStatus.OK);
}
}
I need to read the Json sent by my frontend, checking and responding with OK, no problems. But I can not read Json. I'm trying to store it in the variable "maoe"
You are trying to do send a GET request to a resource that accepts only POST requests. That is why you are getting a 405 response. Change either your rest service or angular http service to have both matching request types.
I'm tring to configure ad OAUTH2 provider with grails based on plugin grails-spring-security-oauth2-provider but upgraded to pring-Security-OAuth M6.
I was able to register clients and get authorization code using /oauth/authorize endpoint.
But I have a problem when I try to obtain access token, it seems it can't return json.
I call the access token endpoint with curl
curl -k -i -H "Accept: application/json" "https://graph.mysite.it/oauth/token?client_id=testApp&client_secret=testAppSecret&response_type=token&grant_typization_code&code=OJD7xf&redirect_uri=https%3A%2F%2Fgraph.mysiste.it%2Fxme"
And server reply with HttpMediaTypeNotAcceptableException Could not find acceptable representation.
Searching on google I have tried adding in resources.xml mvc:annotation-driven to
let spring register json jackson convertor, at this point the call return with HTTP/1.1 406 Not Acceptable "The resource identified by this request is only capable of generating responses with characteristics not acceptable according to the request "accept" headers"
Going into spring security oauth source I reached this controller TokenEndpoint.java
Debugging here I see the token is correctly generated.
Using groovy console I have tried manually colling jackson converter ad it worked:
import org.codehaus.jackson.map.ObjectMapper
import org.springframework.security.oauth2.common.OAuth2AccessToken
def mapper = new ObjectMapper()
def token = new OAuth2AccessToken('fdkshdlfhklsahdklfhksaldfkl')
mapper.writeValueAsString(token)
The json is correctly printed, so I can exclude a problem in jackson configuration.
The spring mvc controller is mapped in grails with
"/oauth/token"(uri:"/oauth/token.dispatch")
Where the problem is? Why grails can't return the json?
This is my Dependecy report
I faced the same problem, but declared annotationHandlerAdapter configuration in resource.groovy this way and it works:
import org.springframework.http.converter.ByteArrayHttpMessageConverter
import org.springframework.http.converter.FormHttpMessageConverter
import org.springframework.http.converter.StringHttpMessageConverter
import org.springframework.http.converter.json.MappingJacksonHttpMessageConverter
import org.springframework.http.converter.xml.SourceHttpMessageConverter
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter
beans = {
annotationHandlerAdapter(RequestMappingHandlerAdapter){
messageConverters = [
new StringHttpMessageConverter(writeAcceptCharset: false),
new ByteArrayHttpMessageConverter(),
new FormHttpMessageConverter(),
new SourceHttpMessageConverter(),
new MappingJacksonHttpMessageConverter()
]
}
}
Solved.
In Spring the converters are declared in bean RequestMappingHandlerAdapter that was configured in grails core in file ControllersGrailsPlugin.
Declaring in resource.xml has no effect.
To add converters I have redefined the bean in a doWithSpring method of my grails plugin
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter
import org.springframework.http.converter.StringHttpMessageConverter
import org.springframework.http.converter.ByteArrayHttpMessageConverter
import org.springframework.http.converter.FormHttpMessageConverter
import org.springframework.http.converter.xml.SourceHttpMessageConverter
import org.springframework.http.converter.json.MappingJacksonHttpMessageConverter
class MyGrailsPlugin {
def doWithSpring = {
annotationHandlerAdapter(RequestMappingHandlerAdapter){
messageConverters = [
new StringHttpMessageConverter(writeAcceptCharset: false),
new ByteArrayHttpMessageConverter(),
new FormHttpMessageConverter(),
new SourceHttpMessageConverter(),
new MappingJacksonHttpMessageConverter()
]
}
}
}