Getting 401 error while calling spring-boot api from angular app

I am working on a springboot+angular app with Okta authentication but I am getting 401 Unauthorized error. Following is my code on the front end side:

app-routing.module.ts

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { OktaCallbackComponent } from '@okta/okta-angular';
import { OktaAuthGuard } from './app.guard';
import { UserDetailsComponent } from './components/user-details/user-details.component';

const routes: Routes = [
 { path: 'home', canActivate: [OktaAuthGuard], component: UserDetailsComponent },
 { path: 'def', component: OktaCallbackComponent },
 { path: '**', redirectTo: '', pathMatch: 'full' },
];

@NgModule({
 imports: [RouterModule.forRoot(routes)],
 exports: [RouterModule]
})
export class AppRoutingModule { }

app.component.ts

import { Component } from '@angular/core';
import { OktaAuthService } from '@okta/okta-angular';

@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent {
title = 'users';
isAuthenticated: boolean;
constructor(public oktaAuth: OktaAuthService) {
	// subscribe to authentication state changes
	this.oktaAuth.$authenticationState.subscribe(
	(isAuthenticated: boolean)  => this.isAuthenticated = isAuthenticated
	);
}
async ngOnInit() {
	// get authentication state for immediate use
	this.isAuthenticated = await this.oktaAuth.isAuthenticated();
}
async login() {
	await this.oktaAuth.signInWithRedirect({
	originalUri: '/users'
	});
}
async logout() {
	await this.oktaAuth.signOut();
}
}

app.guard.ts

import { OktaAuthService } from '@okta/okta-angular';
import { Injectable } from '@angular/core';
import { Router, CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';

@Injectable({
providedIn: 'root'
})
export class OktaAuthGuard implements CanActivate {
oktaAuth;
authenticated;
constructor(private okta: OktaAuthService, private router: Router) {
	this.oktaAuth = okta;
}

async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
	this.authenticated = await this.okta.isAuthenticated();
	console.log('can activate?', this.authenticated);
	if (this.authenticated) { return true; }
	// Redirect to login flow.
	this.okta.signInWithRedirect();
	return false;
}
}

app.module.ts

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
//import { OktaAuthModule, OktaCallbackComponent } from '@okta/okta-angular';
import {OktaAuthModule, OKTA_CONFIG} from '@okta/okta-angular';
import { UserDetailsComponent } from './components/user-details/user-details.component';
import { ReactiveFormsModule } from '@angular/forms';
import { OktaAuthGuard } from './app.guard';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { XSRFTokenInterceptor } from './xsrf-token-interceptor';
import { AuthInterceptor } from './auth.interceptor';

const oktaConfig = {
issuer: 'https://dev-xxxxxxxx.okta.com/oauth2/default',
redirectUri: window.location.origin + '/users',
clientId: 'xxxxxxxxxxxxxxx',
pkce: true
};

@NgModule({
declarations: [
	AppComponent,
	UserDetailsComponent
],
imports: [
	BrowserModule,
	OktaAuthModule,
	AppRoutingModule,
	HttpClientModule
],
providers: [
	OktaAuthGuard,
	{provide: HTTP_INTERCEPTORS, useClass : XSRFTokenInterceptor, multi: true},
	{ provide: OKTA_CONFIG, useValue: oktaConfig },
	{provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true},],
bootstrap: [AppComponent]
})
export class AppModule { }

auth.interceptor.ts

import { Injectable } from '@angular/core';
import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http';
import { Observable, from } from 'rxjs';
import { OktaAuthService } from '@okta/okta-angular';

@Injectable()
export class AuthInterceptor implements HttpInterceptor {

constructor(private oktaAuth: OktaAuthService) {
}

intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
	return from(this.handleAccess(request, next));
}

private async handleAccess(request: HttpRequest<any>, next: HttpHandler): Promise<HttpEvent<any>> {

	const accessToken = await this.oktaAuth.getAccessToken();

	if (accessToken) {
	console.log('token: ' + accessToken);
	request = request.clone({
		setHeaders: {
		Authorization: 'Bearer ' + accessToken
		}
	});
	}
	return next.handle(request).toPromise();
}
}

user.service.ts

 import { HttpClient } from '@angular/common/http';
 import { Injectable } from '@angular/core';
 import { Observable } from 'rxjs';
 import { environment } from 'src/environments/environment';
 import { map } from 'rxjs/operators';
 import { HandleError, HttpErrorHandler } from './http-error-handler.service';
 import { catchError } from 'rxjs/operators';

@Injectable({
providedIn: 'root'
})
export class UserService {
private handleError: HandleError;
contactsUrl = environment.contactAPI;
constructor(private http: HttpClient,
	httpErrorHandler: HttpErrorHandler) {
	this.handleError = httpErrorHandler.createHandleError('HeroesService');
}

getUsers() {
 return this.http.get<any>(this.contactsUrl)
    .pipe(map(info => {
        console.log("info: "+info);            
        return info;
    }));

}}

xsrf-token-interceptor.ts

 import { Injectable } from '@angular/core';
 import {HttpEvent, HttpRequest, HttpHandler,HttpInterceptor, HttpErrorResponse, 
  HttpXsrfTokenExtractor} from '@angular/common/http';
 import { Observable } from 'rxjs';

@Injectable()
export class XSRFTokenInterceptor implements HttpInterceptor {

constructor(private tokenExtractor: HttpXsrfTokenExtractor) {}

intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
     return next.handle(req);
   }
}

At the backend:

SecurityDevConfiguartion.java

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Profile;
import org.springframework.http.HttpMethod;
import 
org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.web.cors.CorsConfiguration;
import java.util.Arrays;


@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)

public class SecurityDevConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
    System.out.println("configure");
    http.cors().and().csrf().disable();
  try {
        http.antMatcher("/**")
                .authorizeRequests()
                .antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
                .antMatchers("/").permitAll()
                .anyRequest().authenticated();
    }catch(Exception e){
        e.printStackTrace();
    }
    http.oauth2ResourceServer();
}

@Bean
CorsConfiguration corsConfiguration() {
    CorsConfiguration corsConfiguration  = new CorsConfiguration();
    corsConfiguration.setAllowCredentials(true);
    corsConfiguration.setAllowedOrigins(Arrays.asList("http://localhost:4200"));
    corsConfiguration.setAllowedHeaders(Arrays.asList("Origin", "Access-Control, Allow-Origin", "Content-Type", "Accept", "Authorization", "Origin, Accept", "X-Requested-With", "Access-Control-Request-Method", "Access-Control-Request-Header" )); 
    corsConfiguration.setExposedHeaders(Arrays.asList("Origin", "Content-Type", "Accept", "Authorization", "Access-Control-Request-Allow-Origin", "Access-Control-Allow-Credentials"));
    corsConfiguration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
    
    return corsConfiguration ;
}
}

UsersController.java

import com.example.users.dao.usersDAO;
import com.example.users.model.Users;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;
import java.security.Principal;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

@RestController
@CrossOrigin(origins = "http://localhost:4200")
public class UsersController {

@Autowired
usersDAO usersDAO;

@RequestMapping(value = "/getUsers", method = RequestMethod.GET)
@ResponseBody
public List getUsers(@AuthenticationPrincipal Principal userInfo) throws Exception {
    System.out.println("userInfo: "+userInfo);
    List<Users> users = usersDAO.findAll();
    List<String> l = new ArrayList();
    for(Iterator i=Users.iterator();i.hasNext();){
        Users u = (Users)(i.next());
        l.add(u.getName());
    }
    return l;
}
}

I have PKCE enabled, grant type is ‘Authorization’ and authorization server is default. ‘http//localhost:4200’ is mentioned in trusted origins in Okta security.

application.properties has client-id and issuer mentioned.

What am I missing/doing wrong? Any help is appreciated.

If you use your browser’s developer tools, do you see the access token getting passed to your API? If so, and it’s still not working, maybe sure you’re synching your computer’s clock with an internet time service.

@mraible - I did

console.log('request header: '+request.headers.get('Authorization'));

from handleAccess method in auth.interceptor.ts and I see the request header printed on my console with the Bearer string followed by the token.
My computer clock is synched up with the internet. Does that make a difference?

Printing it is one thing - do you see it as an actual header in the network tab when the request is made? If so, then it’s probably something on the server side. The clock needs to be synced with the internet because Okta’s JWTs are time-sensitive (because they’re created from a synched clock).

@mraible
yes, I can see it in the network tab:

Capture

OK, so now let’s look at your Spring Boot side of things. Are you using the Okta Spring Boot starter? Do you have a SecurityConfiguration class?

@mraible

yes,

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Profile;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.web.cors.CorsConfiguration;
import java.util.Arrays;


@EnableWebSecurity
public class SecurityDevConfiguration extends WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
    System.out.println("configure");
    http.cors().and().csrf().disable();
    http.cors().configurationSource(request -> new CorsConfiguration(corsConfiguration()));
    
    try {
        http.antMatcher("/**")
                .authorizeRequests()
                .antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
                .antMatchers("/").permitAll()
                .anyRequest().authenticated();
    }catch(Exception e){
        e.printStackTrace();
    }
    http.oauth2ResourceServer();
}

@Bean
CorsConfiguration corsConfiguration() {
    CorsConfiguration corsConfiguration  = new CorsConfiguration();
    corsConfiguration.setAllowCredentials(true);
    corsConfiguration.setAllowedOrigins(Arrays.asList("http://localhost:4200"));
    corsConfiguration.setAllowedHeaders(Arrays.asList("Origin", "Access-Control, Allow-Origin", "Content-Type", "Accept", "Authorization", "Origin, Accept", "X-Requested-With", "Access-Control-Request-Method", "Access-Control-Request-Header" )); 
    corsConfiguration.setExposedHeaders(Arrays.asList("Origin", "Content-Type", "Accept", "Authorization", "Access-Control-Request-Allow-Origin", "Access-Control-Allow-Credentials"));
    corsConfiguration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
    
    return corsConfiguration ;
}

}

I don’t think you need the try/catch in your configure() method. Also, try adding .jwt() to oauth2ResourceServer(). For example:

http.oauth2ResourceServer().jwt()

@mraible I removed the try/catch and replaced

  http.oauth2ResourceServer();

with

 http.oauth2ResourceServer().jwt();

but didn’t help. Still getting 401 :frowning:

Is there any way you can share your project on GitHub? That way I can clone it and debug.

Sure, I’ll. Thank you for your help and time!

@mraible

I shared my front-end (shweta119/users-frontend) and back-end (shweta119/users-backend) code on GitHub with you. You should have received an invite to view the code. Please let me know if you have any issue accessing the code.

I took a look at your users-frontend project. The first thing I noticed is it’s kinda funny that you’re mapping the callback to /users. Any reason why you’re doing that instead of /callback? It seems like you’re trying to give this URL more meaning than just logging in the user.

Unfortunately, I’m unable to run your frontend due to the following error:

ERROR in ./src/styles.scss (./node_modules/@angular-devkit/build-angular/src/angular-cli-files/plugins/raw-css-loader.js!./node_modules/postcss-loader/src??embedded!./node_modules/sass-loader/lib/loader.js??ref--14-3!./src/styles.scss)
Module build failed (from ./node_modules/sass-loader/lib/loader.js):
Error: Cannot find module 'node-sass'
Require stack:

I’m using Node.js v14.17.0 and npm v6.14.13.

I’ll look at the backend now.

I was able to prove your backend app works if you provide a valid access token to it.

First, I removed all the Oracle and JPA dependencies in its pom.xml. I also removed spring-security-oauth2 since it’s not needed. Then, I modified your application.properties so it only has okta.oauth2.* keys/values in it.

Then, I modified the Users class to remove all JPA annotations so it’s just a POJO. I did add a constructor to it to simplify things.

package com.example.users.model;

public class Users {

    private Integer id;
    private String name;
    private String email;
    private String details;

    public Users(Integer id, String name) {
        this.id = id;
        this.name = name;
    }

    // getters and setters
}

Then, I modified the UsersController to simply create objects and return them.

package com.example.users.controller;

import com.example.users.model.Users;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

import java.security.Principal;
import java.util.List;

@RestController
public class UsersController {

    @RequestMapping(value = "/users/getUsers", method = RequestMethod.GET)
    @ResponseBody
    public List<Users> getUsers(@AuthenticationPrincipal Principal userInfo) {

        System.out.println("userInfo: " + userInfo);
        Users user1 = new Users(1, "one");
        Users user2 = new Users(2, "two");

        return List.of(user1, user2);
    }
}

Next, I modified my SPA app on Okta to enable implicit flow, allowed returning the access token for implicit, and added https://oidcdebugger.com/debug as a redirect URI.

Then, I opened my browser to https://oidcdebugger.com and entered my information to get an access token.

Once, I got the access token, I opened a terminal window and set it as a variable.

TOKEN=eyJraWQiOiJYa2pXdjMzTDRB...

Then, I used HTTPie to send that access token to the server and got back results.

$ http :8080/users/getUsers "Authorization: Bearer ${TOKEN}"
HTTP/1.1 200
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Connection: keep-alive
Content-Type: application/json
Date: Tue, 25 May 2021 00:34:13 GMT
Expires: 0
Keep-Alive: timeout=60
Pragma: no-cache
Transfer-Encoding: chunked
Vary: Origin
Vary: Access-Control-Request-Method
Vary: Access-Control-Request-Headers
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 1; mode=block

[
    {
        "details": null,
        "email": null,
        "id": 1,
        "name": "one"
    },
    {
        "details": null,
        "email": null,
        "id": 2,
        "name": "two"
    }
]

@mraible

I have node.js v10.15.0. I am not sure which package should we compare for npm version.
As for the callback, it is a dummy project mirroring my original project. So, I didn’t put much thought into the callback url.
I didn’t get the error for ‘node-sass’ but

npm install node-sass

may be the solution for the error.

@mraible This is great that the back-end works. As I see and believe, front-end should also be okay. But then why an authorization error when calling the method from front end? Is there some setting in OKTA that I am doing wrong? I have been trying to figure this out for quite sometime now but I see no solution. Please help!

I had to install node-sass v4 to get the frontend to startup.

npm i node-sass@4

After doing this, I was able to login just fine. Changes I made:

  1. In app.module.ts, I added my Okta settings and changed the redirect URI to window.location.origin + '/callback'. I also removed OktaAuthGuard from the providers list.
  2. In app-routing.module.ts, I changed the users route to callback. { path: 'callback', component: OktaCallbackComponent }
  3. Now, when I navigate to http://localhost:4200/home, it says “user-details works!” and returns data from the backend in my console.

Thank you! So, looks like there were no major changes. I made changes as you did for angular app above and I still get 401. So, at least we know that the front-end and back-end code work perfectly fine. I guess I’ll look at OKTA settings if there is something that is hindering the calls. Thank you once again for all your help!!!

This topic was automatically closed 24 hours after the last reply. New replies are no longer allowed.