Overriding default login page of spring security OAuth2 while using new Authorization Server and PKCE - reactjs

I'm using React JS as client and my Authorization server uses new OAuth2 with PKCE. The whole flow works but during authentication process, spring security returns me default login page. I have made a change to get the custom React JS login page but the question is, how to handle the submit of the login page from React JS? I am using PKCE so once I submit login page, I need to get the 'code' back in response which I need to further pass (along with verification code) to request JWT Token. Application details are given below,
I got a separate Authorization server OAuth2 using <artifactId>spring-security-oauth2-authorization-server</artifactId>. Authorization server issues JWT upon successful authentication using PKCE. I'm using PKCE because front end is React JS and I do not wish to save client secret at Front end. This is how the flow works (without explaining minute details),
UI calls ${AUTHORIZATION_SERVER_BASE_URL}/oauth2/authorize?response_type=code&client_id=${CLIENT_ID}&scope=openid&redirect_uri=${REDIRECT_URI}&code_challenge=${codeChallenge}&code_challenge_method=S256;
Spring gives back default login page
User enters credentials
Upon successful authentication, spring returns 'code' to the specified 'REDIRECT_URI'
I make Axios POST request passing 'code' and original code_verifier to get JWT Token eg endpoint post(${AUTHORIZATION_SERVER_BASE_URL}/oauth2/token, paramsEncoded, axiosConfig)
AuthorizationServerConfig class is pasted below.
#Configuration
#AllArgsConstructor
public class AuthorizationServerConfig {
private final CORSCustomizer corsCustomizer;
#Bean
#Order(Ordered.HIGHEST_PRECEDENCE)
public SecurityFilterChain authServerSecurityFilterChain(HttpSecurity http) throws Exception {
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
corsCustomizer.corsCustomizer(http);
return http.formLogin().loginPage("http://127.0.0.1:3000/login").and().build();
//return http.formLogin(Customizer.withDefaults()).build();
}
#Bean
public RegisteredClientRepository registeredClientRepository() {
RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
.clientId("client")
.clientAuthenticationMethod(ClientAuthenticationMethod.NONE)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
.redirectUri("http://127.0.0.1:3000/authorized")
.scope(OidcScopes.OPENID)
.clientSettings(ClientSettings.builder()
.requireAuthorizationConsent(true).build())
.tokenSettings(TokenSettings.builder()
.refreshTokenTimeToLive(Duration.ofHours(10))
.build())
.build();
return new InMemoryRegisteredClientRepository(registeredClient);
}
#Bean
public ProviderSettings providerSettings() {
return ProviderSettings.builder()
.issuer("http://auth-server:3001")
.build();
}
#Bean
public JWKSource<SecurityContext> jwkSource() throws NoSuchAlgorithmException {
RSAKey rsaKey = JwksKeys.generateRSAKey();
JWKSet jwkSet = new JWKSet(rsaKey);
return (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
}
#Bean
OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer() {
return context -> {
if (context.getTokenType() == OAuth2TokenType.ACCESS_TOKEN) {
Authentication principal = context.getPrincipal();
Set<String> authorities = principal.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toSet());
context.getClaims().claim("roles", authorities);
}
};
}
}

Related

Authenticate and access spring security protected API from a react js application

My objective is to access spring security protected API from a react js application after authentication.
Spring boot application hosted at http://myserver:8080
React JS application is hosted at http://myserver:3000
I am able to authenticate and access the APIs using curl as follows:
Access login url with credentials . Extract jsessionid token from response header.
Access end url with jsessionid token.
$ curl -i -X POST login_url --data 'username=myusername&password=mypassword'
$ curl end_url -H 'Cookie: JSESSIONID=session_token'
I am trying to replicate the same through React JS Application.
Even though JSESSIONID Cookie is present in response header (verified through curl , and browser dev tools) but axios response header is not able to capture it.
I understand that "Set-Cookie" header in JavaScript code will not work by default. As discussed in this question React Axios, Can't read the Set-Cookie header on axios response
Kindly help with modification required in code to achieve the same. OR suggest alternate way to achieve the objective.
Thanks.
Client side code is as follows:
const onSubmitAuthenticateButton = (e) => {
e.preventDefault();
const loginUrl = 'http://myserver:8080/login';
axios.defaults.withCredentials = true;
axios.post(loginUrl, { username, password})
.then(res => console.log(res.headers))
.catch(err => console.log(err.message));
}
In Spring Secuirty configuration, csrf is disabled and cors allowed origin for "http://myserver:3000".
WebSecurityConfig class
#Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter{
private CustomAuthenticationProvider customAuthProvider;
public WebSecurityConfig(CustomAuthenticationProvider customAuthProvider) {
super();
this.customAuthProvider = customAuthProvider;
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.cors()
.and()
.csrf().disable()
.authorizeRequests()
.anyRequest().fullyAuthenticated()
.and()
.formLogin();
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(customAuthProvider);
}
}
WebMvcConfig class
#Configuration
public class WebMvcConfig implements WebMvcConfigurer {
private final long MAX_AGE_SECS = 3600;
#Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("http://myserver:3000")
.allowedMethods("GET", "POST")
.exposedHeaders("Set-Cookie")
.maxAge(MAX_AGE_SECS)
.allowCredentials(true);
}
}
I have achieved the objective through alternate way.
Instead of session based authentication, i am now using stateless authentication. Upon successful authentication a jwt token is returned as response. Subsequent API call, the jwt token is attached as payload. The application checks for the validity of token before processing the API call request.

How to authenticate users with Social Login (Facebook, Google) in a REST API backend with Spring Boot (separate React Frontend)

I am currently working on a Spring Boot REST API. I have successfully added login using client credentials with Spring Oauth and Spring Security (I can successfully get access token and refresh token using /oauth/token endpoint).
But now I want to provide social login with Facebook and Google. As I understand, this is the flow.
User clicks Login with Social button in React frontend.
Then, he will be asked to grant access. (Still in React)
After that he will be redirected to the react front end with an access token.
Frontend sends that access token to the Spring Boot backend. (I don't know to what endpoint)
Then backend uses that access token to fetch details from the Facebook/Google and check whether a such user exists in our database.
If such user exists, backend will return access and refresh tokens to the frontend.
Now frontend can consume all the endpoints.
My problem is, I have no idea about the steps 4,5 and 6.
Do I have to make a custom endpoint to receive FB/Google access tokens?
How do I issue custom access and refresh tokens in Spring Boot?
I would really appreciate it if you could help me with this scenario.
The flow it's the following:
Front-End calls spring to /oauth2/authorization/facebook(or whatever client do you wanna use)
Back-end respond with a redirect to Facebook login page(including in the query params, client_id, scope, redirect_uri(must be the same present on your developer console) and state which is used to avoid XSRF attacks, according to OAuth2 Standards)
you can see more details here https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.1
state
RECOMMENDED. An opaque value used by the client to maintain
state between the request and callback. The authorization
server includes this value when redirecting the user-agent back
to the client. The parameter SHOULD be used for preventing
cross-site request forgery as described in Section 10.12.
3) Once the user log-in and accept whatever popup facebook or other services will show, the user will be redirected to the page present in "redirect_uri", this page should be a component of your ReactJs. The callback will come with some datas put in the query params, usually those params are two, state(it's the same you sent to facebook) and code(which is used from the BE to end the login flow).
Once facebook or whatever service, called you back, you have to take those 2 params, from the url(using JS for instance) and call the /login/oauth2/code/facebook/?code=CODE_GENERATED_BY_FACEBOOK&?state=STATE_GENERATED_BY_SPRING
Spring will call the facebook service(with an implementation of OAuth2AccessTokenResponseClient, using your secret_token, client_id, code and few other fields. Once facebook responds with the access_token and refresh_token, spring call an implementation of OAuth2UserService, used to get user info from facebook using the access_token created a moment before, at facebook's response a session will be created including the principal. (You can intercept the login success creating an implementation of SimpleUrlAuthenticationSuccessHandlerand adding it to your spring security configuration. (For facebook, google and otka in theory OAuth2AccessTokenResponseClient and OAuth2UserService implementations should already exist.
In that handler you can put the logic to add and look for an existing user.
coming back to the default behavior
Once spring created the new session and gave you the JSESSIONID cookie, it will redirect you to the root (I believe, I don't remember exactly which is the default path after the login, but you can change it, creating your own implementation of the handler I told you before)
Note: access_token and refresh_token will be stored in a OAuth2AuthorizedClient, stored in the ClientRegistrationRepository.
This is the end. From now then you can call your back end with that cookie and the be will see you as a logged user. My suggestion is once you got the simple flow working, you should implement a JWT token to use and store in the localstorage of your browser instead of using the cookie.
Hopefully I gave you the infos you were looking for, if I missed something, misunderstood something or something it's not clear let me know in the comment.
UPDATE (some java samples)
My OAuth2 SecurityConfig :
NOTE:
PROTECTED_URLS it's just : public static final RequestMatcher PROTECTED_URLS = new NegatedRequestMatcher(PUBLIC_URLS);
PUBLIC_URLS it's just: private static final RequestMatcher PUBLIC_URLS = new OrRequestMatcher( new AntPathRequestMatcher("/api/v1/login"));
Also note I'm using a dual HttpSecurity configuration. (But in this case it's useless to public that too)
#Configuration
#EnableWebSecurity
#RequiredArgsConstructor
public class OAuth2ClientSecurityConfiguration extends WebSecurityConfigurerAdapter {
private final JWTService jwtService;
private final TempUserDataService tempUserDataService;
private final OAuth2AuthorizedClientRepo authorizedClientRepo;
private final OAuth2AuthorizedClientService clientService;
private final UserAuthenticationService authenticationService;
private final SimpleUrlAuthenticationSuccessHandler successHandler; //This is the default one, this bean has been created in another HttpSecurity Configuration file.
private final OAuth2TokenAuthenticationProvider authenticationProvider2;
private final CustomOAuth2AuthorizedClientServiceImpl customOAuth2AuthorizedClientService;
private final TwitchOAuth2UrlAuthSuccessHandler oauth2Filter; //This is the success handler customized.
//In this bean i set the default successHandler and the current AuthManager.
#Bean("oauth2TokenAuthenticaitonFilter")
TokenAuthenticationFilter oatuh2TokenAuthenticationFilter() throws Exception {
TokenAuthenticationFilter filter = new TokenAuthenticationFilter(PROTECTED_URLS);
filter.setAuthenticationSuccessHandler(successHandler);
filter.setAuthenticationManager(authenticationManager());
return filter;
}
#PostConstruct
public void setFilterSettings() {
oauth2Filter.setRedirectStrategy(new NoRedirectStrategy());
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authenticationProvider2);
}
#Bean
public RestOperations restOperations() {
return new RestTemplate();
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and().authorizeRequests().antMatchers("/twitch/**").authenticated()
.and().csrf().disable()
.formLogin().disable()
.httpBasic().disable()
.logout().disable().authenticationProvider(authenticationProvider2)
.exceptionHandling()
.authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED))
.and()
.addFilterBefore(oatuh2TokenAuthenticationFilter(), AnonymousAuthenticationFilter.class)
.oauth2Login().successHandler(oauth2Filter).tokenEndpoint()
.accessTokenResponseClient(new RestOAuth2AccessTokenResponseClient(restOperations()))
.and().authorizedClientService(customOAuth2AuthorizedClientService)
.userInfoEndpoint().userService(new RestOAuth2UserService(restOperations(), tempUserDataService, authorizedClientRepo));
}
#Bean
FilterRegistrationBean disableAutoRegistrationOAuth2Filter() throws Exception {
FilterRegistrationBean registration = new FilterRegistrationBean(oatuh2TokenAuthenticationFilter());
registration.setEnabled(false);
return registration;
}
}
By the fact that my SessionCreationPolicy.STATELESS the cookie created by spring after the end of the OAuth2 Flow is useless. So once the process its over I give to the user a TemporaryJWT Token used to access the only possible service (the register service)
My TokenAutheticationFilter:
public class TokenAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
private static final String AUTHORIZATION = "Authorization";
private static final String BEARER = "Bearer";
public TokenAuthenticationFilter(RequestMatcher requiresAuthenticationRequestMatcher) {
super(requiresAuthenticationRequestMatcher);
}
#Override
public Authentication attemptAuthentication(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws AuthenticationException, IOException, ServletException {
String token = Optional.ofNullable(httpServletRequest.getHeader(AUTHORIZATION))
.map(v -> v.replace(BEARER, "").trim())
.orElseThrow(() -> new BadCredentialsException("Missing authentication token."));
Authentication auth = new UsernamePasswordAuthenticationToken(token, token);
return getAuthenticationManager().authenticate(auth);
}
#Override
protected void successfulAuthentication(
HttpServletRequest request,
HttpServletResponse response,
FilterChain chain,
Authentication authResult) throws IOException, ServletException {
super.successfulAuthentication(request, response, chain, authResult);
chain.doFilter(request, response);
}
#Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
response.setStatus(401);
}
}
TwitchOAuth2UrlAuthSuccessHandler (This is where all the magic happens):
This handler is called once the userService and the userService is called when the user calls api.myweb.com/login/oauth2/code/facebook/?code=XXX&state=XXX. (please don't forget the state)
#Component
#RequiredArgsConstructor
public class TwitchOAuth2UrlAuthSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
private final OAuth2AuthorizedClientRepo oAuth2AuthorizedClientRepo;
private final UserAuthenticationService authenticationService;
private final JWTService jwtService;
private final Gson gson;
#Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
super.onAuthenticationSuccess(request, response, authentication);
response.setStatus(200);
Cookie cookie = new Cookie("JSESSIONID", null);
cookie.setHttpOnly(true);
cookie.setSecure(true);
cookie.setPath("/");
cookie.setMaxAge(0);
response.addCookie(cookie);
Optional<OAuth2AuthorizedClientEntity> oAuth2AuthorizedClient = oAuth2AuthorizedClientRepo.findById(new OAuth2AuthorizedClientId(((OAuth2AuthenticationToken) authentication).getAuthorizedClientRegistrationId(), authentication.getName()));
if (oAuth2AuthorizedClient.isPresent() && oAuth2AuthorizedClient.get().getUserDetails() != null) {
response.getWriter().write(gson.toJson(authenticationService.loginWithCryptedPassword(oAuth2AuthorizedClient.get().getUserDetails().getUsername(), oAuth2AuthorizedClient.get().getUserDetails().getPassword())));
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
response.getWriter().flush();
} else {
response.setHeader("Authorization", jwtService.createTempToken(((OAuth2AuthenticationToken) authentication).getAuthorizedClientRegistrationId(), authentication.getName()));
}
}
#Override
protected String determineTargetUrl(HttpServletRequest request, HttpServletResponse response) {
return "";
}
}
RestOAuth2AccessTokenResponseClient (its responsable to take Access_token and refresh_token from FB)
public class RestOAuth2AccessTokenResponseClient implements OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> {
private final RestOperations restOperations;
public RestOAuth2AccessTokenResponseClient(RestOperations restOperations) {
this.restOperations = restOperations;
}
#Override
public OAuth2AccessTokenResponse getTokenResponse(OAuth2AuthorizationCodeGrantRequest authorizationGrantRequest) {
ClientRegistration clientRegistration = authorizationGrantRequest.getClientRegistration();
String tokenUri = clientRegistration.getProviderDetails().getTokenUri();
MultiValueMap<String, String> tokenRequest = new LinkedMultiValueMap<>();
tokenRequest.add("client_id", clientRegistration.getClientId());
tokenRequest.add("client_secret", clientRegistration.getClientSecret());
tokenRequest.add("grant_type", clientRegistration.getAuthorizationGrantType().getValue());
tokenRequest.add("code", authorizationGrantRequest.getAuthorizationExchange().getAuthorizationResponse().getCode());
tokenRequest.add("redirect_uri", authorizationGrantRequest.getAuthorizationExchange().getAuthorizationRequest().getRedirectUri());
tokenRequest.add("scope", String.join(" ", authorizationGrantRequest.getClientRegistration().getScopes()));
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
headers.add(HttpHeaders.USER_AGENT, "Discord Bot 1.0");
ResponseEntity<AccessResponse> responseEntity = restOperations.exchange(tokenUri, HttpMethod.POST, new HttpEntity<>(tokenRequest, headers), AccessResponse.class);
if (!responseEntity.getStatusCode().equals(HttpStatus.OK) || responseEntity.getBody() == null) {
throw new SecurityException("The result of token call returned error or the body returned null.");
}
AccessResponse accessResponse = responseEntity.getBody();
Set<String> scopes = accessResponse.getScopes().isEmpty() ?
authorizationGrantRequest.getAuthorizationExchange().getAuthorizationRequest().getScopes() : accessResponse.getScopes();
return OAuth2AccessTokenResponse.withToken(accessResponse.getAccessToken())
.tokenType(accessResponse.getTokenType())
.expiresIn(accessResponse.getExpiresIn())
.refreshToken(accessResponse.getRefreshToken())
.scopes(scopes)
.build();
}
}
UserService
public class RestOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {
private final RestOperations restOperations;
private final TempUserDataService tempUserDataService;
private final OAuth2AuthorizedClientRepo authorizedClientRepo;
public RestOAuth2UserService(RestOperations restOperations, TempUserDataService tempUserDataService, OAuth2AuthorizedClientRepo authorizedClientRepo) {
this.restOperations = restOperations;
this.tempUserDataService = tempUserDataService;
this.authorizedClientRepo = authorizedClientRepo;
}
#Override
public OAuth2User loadUser(OAuth2UserRequest oAuth2UserRequest) throws OAuth2AuthenticationException {
String userInfoUrl = oAuth2UserRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUri();
HttpHeaders headers = new HttpHeaders();
headers.add(HttpHeaders.AUTHORIZATION, String.format("Bearer %s", oAuth2UserRequest.getAccessToken().getTokenValue()));
headers.add(HttpHeaders.USER_AGENT, "Discord Bot 1.0");
if (oAuth2UserRequest.getClientRegistration().getClientName().equals("OAuth2 Twitch")) {
headers.add("client-id", oAuth2UserRequest.getClientRegistration().getClientId());
}
ParameterizedTypeReference<Map<String, Object>> typeReference = new ParameterizedTypeReference<Map<String, Object>>() {
};
ResponseEntity<Map<String, Object>> responseEntity = restOperations.exchange(userInfoUrl, HttpMethod.GET, new HttpEntity<>(headers), typeReference);
if (!responseEntity.getStatusCode().equals(HttpStatus.OK) || responseEntity.getBody() == null) {
throw new SecurityException("The result of token call returned error or the body returned null.");
}
Map<String, Object> userAttributes = responseEntity.getBody();
userAttributes = LinkedHashMap.class.cast(((ArrayList) userAttributes.get("data")).get(0));
OAuth2AuthorizedClientId clientId = new OAuth2AuthorizedClientId(oAuth2UserRequest.getClientRegistration().getRegistrationId(), String.valueOf(userAttributes.get("id")));
Optional<OAuth2AuthorizedClientEntity> clientEntity = this.authorizedClientRepo.findById(clientId);
if (!clientEntity.isPresent() || clientEntity.get().getUserDetails() == null) {
TempUserData tempUserData = new TempUserData();
tempUserData.setClientId(clientId);
tempUserData.setUsername(String.valueOf(userAttributes.get("login")));
tempUserData.setEmail(String.valueOf(userAttributes.get("email")));
tempUserDataService.save(tempUserData);
}
Set<GrantedAuthority> authorities = Collections.singleton(new OAuth2UserAuthority(userAttributes));
return new DefaultOAuth2User(authorities, userAttributes, oAuth2UserRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName());
}
As asked this is all the code you need, just to give you another hint. When you call /login/oauth2/code/facebook/?code=XXX&?state=XXX the chain is the following:
RestOAuth2AccessTokenResponseClient
RestOAuth2UserService
TwitchOAuth2UrlAuthSuccessHandler
I hope this can help you. Let me know if you need more explainations.

How to handle CORS error in Spring Boot, AngularJS application?

I am implementing a simple authentication program using AngularJS frontend and Spring Boot backend. I am facing an issue while sending the login request. When the relavent request sent, following error prints in the console
Error:-
Access to XMLHttpRequest at 'http://localhost:8080/rest/users/user' from origin 'http://localhost:4200' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.
POST http://localhost:8080/rest/users/user net::ERR_FAILED
Same error occurred when sending the request for registration function. Then I found a solution of adding #CrossOrigin(origins = "http://localhost:4200") to the controller. It fixed the error occured during the registration.
Now the problem is even though I have written the login method in the same controller, it gives me the error while trying to log in and will not gives any error if I try to register new user.
Below is the implementation of the backend
Repository :-
import com.app.cashier.model.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.web.bind.annotation.CrossOrigin;
import java.util.List;
public interface UsersRepository extends JpaRepository<User, Integer> {
List<User> findByUserName(String userName);
}
Resource :-
package com.app.cashier.resource;
import com.app.cashier.model.User;
import com.app.cashier.repository.UsersRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
#CrossOrigin(origins = "http://localhost:4200") //<------------ I addded #CrossOrigin
#RestController
#RequestMapping(value = "/rest/users")
public class UserResource {
#Autowired
UsersRepository usersRepository;
#GetMapping(value = "/all")
public List<User> getAll(){
return usersRepository.findAll();
}
#GetMapping(value = "/user") //<----- Login function. This still gives the above error
public List<User> getUser(#RequestBody final User user){
return usersRepository.findByUserName(user.getUserName());
}
#PostMapping(value = "/load") //<----- Registration function. This gives no error after adding #CrossOrigin
public List<User> persist(#RequestBody final User user){
usersRepository.save(user);
return usersRepository.findAll();
}
}
AngularJS frontend request
login(userName) {
console.log(userName)
return this.http.post<any>(`http://localhost:8080/rest/users/user`, { userName })
.pipe(map(user => {
// store user details and jwt token in local storage to keep user logged in between page refreshes
localStorage.setItem('currentUser', JSON.stringify(user));
this.currentUserSubject.next(user);
console.log(user);
return user;
}));
}
How can I overcome this issue. Massive thanks!
Providing the #CrossOrigin annotation at the controller level should enable cross origin for all the methods under that controller.lar request so it might be because of some additional headers that you are adding for that particular request so try like :
#CrossOrigin(origins = "http://localhost:4200", allowedHeaders = "*")
#RestController
#RequestMapping(value = "/rest/users")
public class UserResource {
//Your code
}
If still having issues then Could you share the url and the headers that you are using to login the new user ?. Also , try having a global cors configuration instead of a controller level one and provide fine grained properties like the methods that you want to expose. Provide the following in a configuration class :
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurer() {
#Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/greeting-javaconfig").allowedOrigins("http://localhost:4200");
}
};
}
Similar : CORS policy conflict in Spring boot

How do I secure my Google Cloud Endpoints APIs with Firebase token verification?

My setup:
Java backend hosted on Google App Engine containing APIs that were created using Google Cloud Endpoints
Mobile client applications containing generated client libraries for the endpoints mentioned above. Also integrated with Firebase for authentication and the database.
My intention is that a user of the mobile client applications will be able to log in to the mobile app using Firebase authentication, then connect to any of the backend APIs, which in turn will do some processing and then read or write data to/from the Firebase database.
To secure the APIs on the server, I think I'll have to use the built-in verifyIdToken() method of the Firebase Server SDK to (see Verifying ID Tokens on Firebase) to decode a user's ID token passed from the client application. As verifyIdToken() runs asynchronously, how would I integrate it with an API method in GAE? I have something similar to the following so far:
#ApiMethod(name = "processAndSaveToDB", httpMethod = "post")
public Response processAndSaveToDB(#Named("token") String token) {
Response response = new Response();
// Check if the user is authenticated first
FirebaseAuth.getInstance().verifyIdToken(idToken)
.addOnSuccessListener(new OnSuccessListener() {
#Override
public void onSuccess(FirebaseToken decodedToken) {
String uid = decodedToken.getUid();
// do bulk of processAndSaveToDB() method
})
.addOnFailureListener(new OnFailureListener() {
#Override
public void onFailure(Exception e) {
// throw unauthorized exception
});
return response;
}
As this authentication task is running asynchronously in task queue, you can wait until that task is ended and continue in synchronous way, optionally you can add listeners onSuccess, onFailure and onComplete.
Task<FirebaseToken> authTask = FirebaseAuth.getInstance().verifyIdToken(idToken)
.addOnSuccessListener(new OnSuccessListener() {
#Override
public void onSuccess(Object tr) {//do smtg }
}).addOnFailureListener(new OnFailureListener() {
#Override
public void onFailure(Exception excptn) {//do smtg }
}).addOnCompleteListener(new OnCompleteListener() {
#Override
public void onComplete(Task task) {//do smtg }
});
try {
Tasks.await(authTask);
} catch(ExecutionException | InterruptedException e ){
//handle error
}
FirebaseToken decodedToken = authTask.getResult();

SignalR authentication failed when passing "Bearer" through query string

I'd like to enable authentication in SignalR while the server was hosted in ASP.NET WebAPI which I'm using OAuth Bearer authrntication and the client is AngularJS.
On client side I originally pass the Bearer token through HTTP header and it works well with the WebAPI. But since SignalR JavsScript doesn't support adding HTTP headers in connection (it's because WebSocket doesn't support specifying HTTP headers) I need to pass the Bearer token through query string by using the code like self.connection.qs = { Bearer: 'xxxxxx' };
The problem is on the WebAPI side my SignalR always returned 401 Unauthorized.
Below is what I did on the WebAPI side.
1, I specified OAuthBearerAuthenticationOptions.Provider to QueryStringEnabledOAuthBearerAuthenticationProvider, which is a class I created inherited from OAuthBearerAuthenticationProvider that can retrieve Bearer token from query string. Code as below.
public class QueryStringEnabledOAuthBearerAuthenticationProvider : OAuthBearerAuthenticationProvider
{
private readonly string _name;
public QueryStringEnabledOAuthBearerAuthenticationProvider()
: this(OAuthDefaults.AuthenticationType)
{
}
public QueryStringEnabledOAuthBearerAuthenticationProvider(string name)
{
_name = name;
}
public override Task RequestToken(OAuthRequestTokenContext context)
{
// try to read token from base class (header) if possible
base.RequestToken(context).Wait();
if (string.IsNullOrWhiteSpace(context.Token))
{
// try to read token from query string
var token = context.Request.Query.Get(_name);
if (!string.IsNullOrWhiteSpace(token))
{
context.Token = token;
}
}
return Task.FromResult(null);
}
}
And registered it as below while WebAPI was started.
var options = new OAuthBearerAuthenticationOptions
{
AuthenticationMode = AuthenticationMode.Active,
AuthenticationType = AuthenticationType,
Provider = new QueryStringEnabledOAuthBearerAuthenticationProvider(),
AccessTokenFormat = _accessTokenFormat,
};
config.SuppressDefaultHostAuthentication();
config.Filters.Add(new HostAuthenticationFilter(OAuthDefaults.AuthenticationType));
app.UseOAuthBearerAuthentication(options);
2, In SignalR part I created an authorize attribute as below. Nothing changed just to be used to add break point.
public class BearerAuthorizeAttribute : AuthorizeAttribute
{
public override bool AuthorizeHubConnection(HubDescriptor hubDescriptor, IRequest request)
{
return base.AuthorizeHubConnection(hubDescriptor, request);
}
public override bool AuthorizeHubMethodInvocation(IHubIncomingInvokerContext hubIncomingInvokerContext, bool appliesToMethod)
{
return base.AuthorizeHubMethodInvocation(hubIncomingInvokerContext, appliesToMethod);
}
}
And registered it when WebAPI started as well.
app.Map("/signalr", map =>
{
// Setup the CORS middleware to run before SignalR.
// By default this will allow all origins. You can
// configure the set of origins and/or http verbs by
// providing a cors options with a different policy.
map.UseCors(CorsOptions.AllowAll);
var hubConfiguration = new HubConfiguration
{
// You can enable JSONP by uncommenting line below.
// JSONP requests are insecure but some older browsers (and some
// versions of IE) require JSONP to work cross domain
// EnableJSONP = true
EnableJavaScriptProxies = false
};
// Run the SignalR pipeline. We're not using MapSignalR
// since this branch already runs under the "/signalr"
// path.
map.RunSignalR(hubConfiguration);
// Require authentication for all hubs
var authorizer = new BearerAuthorizeAttribute();
var module = new AuthorizeModule(authorizer, authorizer);
GlobalHost.HubPipeline.AddModule(module);
});
I found, when SignalR connected my QueryStringEnabledOAuthBearerAuthenticationProvider.RequestToken was invoked and it retrieved Bearer token successfully. But then when SignalR BearerAuthorizeAttribute.AuthorizeHubConnection was invoked the parameter request.User still not authenticated. So it returned 401.
Can anyone give me some ideas on what's wrong I did, thanks.
I'm using headers, this is how I solved it
var authData = localStorageService.get('authorizationData');
var token = authData.token;
$.signalR.ajaxDefaults.headers = { Authorization: "Bearer " + token };
Hope it helps
I resolved this problem by unprotect the Bearer token from query string in my AuthorizeAttribute, and set the user principal into a new ServerRequest. For detailed information please check http://blog.shaunxu.me/archive/2014/05/27/set-context-user-principal-for-customized-authentication-in-signalr.aspx
This might not be the best solution but it worked.

Resources