Security Top

I just announced the new Learn Spring Security course, including the full material focused on the new OAuth2 stack in Spring Security 5:

>> CHECK OUT THE COURSE

1. Overview

In this tutorial, we'll secure a REST API with OAuth2 and consume it from a simple Angular client.

The application we're going to build out will consist of 3 separate modules:

  • Authorization Server
  • Resource Server
  • UI authorization code – a front-end application using the Authorization Code Flow

We'll use the OAuth stack in Spring Security 5. If you want to use the Spring Security OAuth legacy stack, have a look at this previous article: Spring REST API + OAuth2 + Angular (using the Spring Security OAuth legacy stack)

Further reading:

Using JWT with Spring Security OAuth

A guide to using JWT tokens with Spring Security 5.

OAuth2.0 and Dynamic Client Registration (using the Spring Security OAuth legacy stack)

Learn how to define clients dynamically with Spring Security and OAuth2.

Alright, let's jump right in.

2. The OAuth2 Authorization Server (AS)

Simply put, an Authorization Server is an application that issues tokens for authorization.

Previously, the Spring Security OAuth stack offered the possibility of setting up an Authorization Server as a Spring Application. But the project has been deprecated, mainly because OAuth is an open standard with many well-established providers such as Okta, Keycloak, and Forgerock to name a few.

Of these, we'll be using Keycloak. It's an open-source Identity and Access Management server administered by RedHat, developed in Java, by JBoss. It supports not only OAuth2 but also other standard protocols such as OpenID Connect and SAML.

For this tutorial, we'll be setting up an embedded Keycloak server in a Spring Boot app.

3. The Resource Server (RS)

Now, let's discuss the resource server; this is essentially the REST API which we ultimately want to be able to consume.

3.1. Maven Configuration

Our Resource Server's pom is much the same as the previous Authorization Server pom, sans the Keycloak part and an additional spring-boot-starter-oauth2-resource-server dependency:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>

3.2. Security Configuration

Since we're using Spring Boot, we can define the minimal required configuration using Boot properties. We'll do this in an application.yml file:

server: 
  port: 8081
  servlet: 
    context-path: /resource-server

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: http://localhost:8083/auth/realms/baeldung
          jwk-set-uri: http://localhost:8083/auth/realms/baeldung/protocol/openid-connect/certs

Here we specified that we'll use JWT tokens for authorization.

The jwk-set-uri property points to the URI containing the public key, so that our Resource Server can verify the tokens' integrity. 

The issuer-uri property represents an additional security measure to validate the issuer of the tokens (which is the Authorization Server) However, adding this property also mandates that the Authorization Server should be running before we can start the Resource Server application.

Next, let's set up a security configuration for the API to secure endpoints:

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.cors()
            .and()
              .authorizeRequests()
                .antMatchers(HttpMethod.GET, "/user/info", "/api/foos/**")
                  .hasAuthority("SCOPE_read")
                .antMatchers(HttpMethod.POST, "/api/foos")
                  .hasAuthority("SCOPE_write")
                .anyRequest()
                  .authenticated()
            .and()
              .oauth2ResourceServer()
                .jwt();
    }
}

As we can see,  for our GET methods, we only allow requests that have read scope. For the POST method, the requester needs to have a write authority in addition to read. However, for any other endpoint, the request should just be authenticated with any user.

Also, the oauth2ResourceServer() method specifies that this is a resource server, with jwt() formatted tokens.

Another point to note here is the use of method cors() to allow Access-Control headers on the requests. This is specially important since we are dealing with an Angular client, and our requests are going to come from another origin URL.

3.4. The Model and Repository

Next, let's define a javax.persistence.Entity for our model, Foo:

@Entity
public class Foo {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;
    
    // constructor, getters and setters
}

Then, we need a repository of Foos. We'll use Spring's PagingAndSortingRepository:

public interface IFooRepository extends PagingAndSortingRepository<Foo, Long> {
}

3.4. The Service and Implementation

After that, we'll define and implement a simple service for our API:

public interface IFooService {
    Optional<Foo> findById(Long id);

    Foo save(Foo foo);
    
    Iterable<Foo> findAll();

}

@Service
public class FooServiceImpl implements IFooService {

    private IFooRepository fooRepository;

    public FooServiceImpl(IFooRepository fooRepository) {
        this.fooRepository = fooRepository;
    }

    @Override
    public Optional<Foo> findById(Long id) {
        return fooRepository.findById(id);
    }

    @Override
    public Foo save(Foo foo) {
        return fooRepository.save(foo);
    }

    @Override
    public Iterable<Foo> findAll() {
        return fooRepository.findAll();
    }
}

3.5. A Sample Controller

Now let's implement a simple controller exposing our Foo resource via a DTO:

@RestController
@RequestMapping(value = "/api/foos")
public class FooController {

    private IFooService fooService;

    public FooController(IFooService fooService) {
        this.fooService = fooService;
    }

    @CrossOrigin(origins = "http://localhost:8089")    
    @GetMapping(value = "/{id}")
    public FooDto findOne(@PathVariable Long id) {
        Foo entity = fooService.findById(id)
            .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
        return convertToDto(entity);
    }

    @GetMapping
    public Collection<FooDto> findAll() {
        Iterable<Foo> foos = this.fooService.findAll();
        List<FooDto> fooDtos = new ArrayList<>();
        foos.forEach(p -> fooDtos.add(convertToDto(p)));
        return fooDtos;
    }

    protected FooDto convertToDto(Foo entity) {
        FooDto dto = new FooDto(entity.getId(), entity.getName());

        return dto;
    }
}

Notice the use of @CrossOrigin above, this is the controller-level config we need to allow CORS from our Angular App running at the specified URL.

Here's our FooDto:

public class FooDto {
    private long id;
    private String name;
}

4. Front End – Setup

We're now going to look at a simple front-end Angular implementation for the client, which will access our REST API.

First, we'll use Angular CLI to generate and manage our front-end modules.

First, we'll install node and npm – as Angular CLI is an npm tool.

Then, we need to use the frontend-maven-plugin to build our Angular project using maven:

<build>
    <plugins>
        <plugin>
            <groupId>com.github.eirslett</groupId>
            <artifactId>frontend-maven-plugin</artifactId>
            <version>1.3</version>
            <configuration>
                <nodeVersion>v6.10.2</nodeVersion>
                <npmVersion>3.10.10</npmVersion>
                <workingDirectory>src/main/resources</workingDirectory>
            </configuration>
            <executions>
                <execution>
                    <id>install node and npm</id>
                    <goals>
                        <goal>install-node-and-npm</goal>
                    </goals>
                </execution>
                <execution>
                    <id>npm install</id>
                    <goals>
                        <goal>npm</goal>
                    </goals>
                </execution>
                <execution>
                    <id>npm run build</id>
                    <goals>
                        <goal>npm</goal>
                    </goals>
                    <configuration>
                        <arguments>run build</arguments>
                    </configuration>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

And finally, generate a new Module using Angular CLI:

ng new oauthApp

In the following section, we will discuss the Angular app logic.

5. Authorization Code Flow Using Angular

We're going to use the OAuth2 Authorization Code flow here.

Our use case is: the client app requests a code from the Authorization Server and is presented with a login page. Once a user provides their valid credentials and submits, the Authorization Server gives us the code. Then the front-end client uses it to acquire an access token.

5.1. Home Component

Lets' start with our main component, the HomeComponent, where all the action starts:

@Component({
  selector: 'home-header',
  providers: [AppService],
  template: `<div class="container" >
    <button *ngIf="!isLoggedIn" class="btn btn-primary" (click)="login()" type="submit">
      Login</button>
    <div *ngIf="isLoggedIn" class="content">
      <span>Welcome !!</span>
      <a class="btn btn-default pull-right"(click)="logout()" href="#">Logout</a>
      <br/>
      <foo-details></foo-details>
    </div>
  </div>`
})
 
export class HomeComponent {
  public isLoggedIn = false;

  constructor(private _service: AppService) { }
 
  ngOnInit() {
    this.isLoggedIn = this._service.checkCredentials();    
    let i = window.location.href.indexOf('code');
    if(!this.isLoggedIn && i != -1) {
      this._service.retrieveToken(window.location.href.substring(i + 5));
    }
  }

  login() {
    window.location.href = 
      'http://localhost:8083/auth/realms/baeldung/protocol/openid-connect/auth?
         response_type=code&scope=openid%20write%20read&client_id=' + 
         this._service.clientId + '&redirect_uri='+ this._service.redirectUri;
    }
 
  logout() {
    this._service.logout();
  }
}

In the beginning, when the user is not logged in, only the login button appears. On clicking this button, the user will be navigated to the AS's authorization URL where they key in username and password. After a successful login, user is redirected back with the authorization code and then we retrieve the access token using this code.

5.2. App Service

Now let's look at AppService – located at app.service.ts – which contains the logic for server interactions:

  • retrieveToken(): to obtain access token using authorization code
  • saveToken(): to save our access token in a cookie using ng2-cookies library
  • getResource(): to get a Foo object from server using its ID
  • checkCredentials(): to check if user is logged in or not
  • logout(): to delete access token cookie and log the user out
export class Foo {
  constructor(public id: number, public name: string) { }
} 

@Injectable()
export class AppService {
  public clientId = 'newClient';
  public redirectUri = 'http://localhost:8089/';

  constructor(private _http: HttpClient) { }

  retrieveToken(code) {
    let params = new URLSearchParams();   
    params.append('grant_type','authorization_code');
    params.append('client_id', this.clientId);
    params.append('client_secret', 'newClientSecret');
    params.append('redirect_uri', this.redirectUri);
    params.append('code',code);

    let headers = 
      new HttpHeaders({'Content-type': 'application/x-www-form-urlencoded; charset=utf-8'});
       
      this._http.post('http://localhost:8083/auth/realms/baeldung/protocol/openid-connect/token', 
        params.toString(), { headers: headers })
        .subscribe(
          data => this.saveToken(data),
          err => alert('Invalid Credentials')); 
  }

  saveToken(token) {
    var expireDate = new Date().getTime() + (1000 * token.expires_in);
    Cookie.set("access_token", token.access_token, expireDate);
    console.log('Obtained Access token');
    window.location.href = 'http://localhost:8089';
  }

  getResource(resourceUrl) : Observable<any> {
    var headers = new HttpHeaders({
      'Content-type': 'application/x-www-form-urlencoded; charset=utf-8', 
      'Authorization': 'Bearer '+Cookie.get('access_token')});
    return this._http.get(resourceUrl, { headers: headers })
                   .catch((error:any) => Observable.throw(error.json().error || 'Server error'));
  }

  checkCredentials() {
    return Cookie.check('access_token');
  } 

  logout() {
    Cookie.delete('access_token');
    window.location.reload();
  }
}

In the retrieveToken method, we use our client credentials and Basic Auth send a POST to the “/openid-connect/token” endpoint to get the access token. The parameters are being sent in a URL encoded format. After we obtain the access token – we store it in a cookie

The cookie storage is especially important here, because we're only using the cookie for storage purposes and not to drive the authentication process directly. This helps protect against cross-site request forgery (CSRF) type of attacks and vulnerabilities.

5.3. Foo Component

Finally, our FooComponent to display our Foo details:

@Component({
  selector: 'foo-details',
  providers: [AppService],  
  template: `<div class="container">
    <h1 class="col-sm-12">Foo Details</h1>
    <div class="col-sm-12">
        <label class="col-sm-3">ID</label> <span>{{foo.id}}</span>
    </div>
    <div class="col-sm-12">
        <label class="col-sm-3">Name</label> <span>{{foo.name}}</span>
    </div>
    <div class="col-sm-12">
        <button class="btn btn-primary" (click)="getFoo()" type="submit">New Foo</button>        
    </div>
  </div>`
})

export class FooComponent {
  public foo = new Foo(1,'sample foo');
  private foosUrl = 'http://localhost:8081/resource-server/api/foos/';  

  constructor(private _service:AppService) {}

  getFoo() {
    this._service.getResource(this.foosUrl+this.foo.id)
      .subscribe(
         data => this.foo = data,
         error =>  this.foo.name = 'Error');
    }
}

5.5. App Component

Our simple AppComponent to act as the root component:

@Component({
  selector: 'app-root',
  template: `<nav class="navbar navbar-default">
    <div class="container-fluid">
      <div class="navbar-header">
        <a class="navbar-brand" href="/">Spring Security Oauth - Authorization Code</a>
      </div>
    </div>
  </nav>
  <router-outlet></router-outlet>`
})

export class AppComponent { }

And the AppModule where we wrap all our components, services and routes:

@NgModule({
  declarations: [
    AppComponent,
    HomeComponent,
    FooComponent    
  ],
  imports: [
    BrowserModule,
    HttpClientModule,
    RouterModule.forRoot([
     { path: '', component: HomeComponent, pathMatch: 'full' }], {onSameUrlNavigation: 'reload'})
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

7. Run the Front End

1. To run any of our front-end modules, we need to build the app first:

mvn clean install

2. Then we need to navigate to our Angular app directory:

cd src/main/resources

3. Finally, we will start our app:

npm start

The server will start by default on port 4200, to change port of any module change the

"start": "ng serve"

in package.json to make it run on port 8089 for example:

"start": "ng serve --port 8089"

8. Conclusion

In this article, we learned how to authorize our application using OAuth2.

The full implementation of this tutorial can be found in the GitHub project.

Security bottom

I just announced the new Learn Spring Security course, including the full material focused on the new OAuth2 stack in Spring Security 5:

>> CHECK OUT THE COURSE
100 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Felipe Borella
Felipe Borella
4 years ago

Hi

How can I run the four modules and test the flow?

Thanks

Eugen Paraschiv
4 years ago
Reply to  Felipe Borella

Hey Felipe – you just need to deploy the modules into a web server together – I’m simply using Eclipse with a Tomcat server for local work. Now – they’re separate apps, so with a bit of configuration they can of course run separately – but yeah, the simplest way to do it is to deploy them together. Cheers,
Eugen.

Felipe Borella
Felipe Borella
4 years ago

Thanks! It worked! o/

Eugen Paraschiv
4 years ago
Reply to  Felipe Borella

Sounds good, happy to help. Cheers,
Eugen.

Craig Doremus
Craig Doremus
3 years ago

Sorry to barge into the middle of this thread, but the OAuth-ng directive seems to have disappeared from the web. Any other ideas?

Eugen Paraschiv
3 years ago
Reply to  Craig Doremus

No idea Craig – I’m not really keeping a close eye on the front-end ecosystem and AngularJS.
One thing I’d recommend is to reach out to the team directly – I’m sure there’s an official channel for that. Cheers,
Eugen.

Felipe Borella
Felipe Borella
4 years ago

Hey Eugen!

I’m trying to run the spring-security-oauth-ui-implicit module.
When I click on the Login Button, I got an error.

Whitelabel Error Page
This application has no explicit mapping for /error, so you are seeing this as a fallback.
Thu Nov 26 10:20:28 BRST 2015
There was an unexpected error (type=Forbidden, status=403).
Access Denied

Any idea?

Thanks
Felipe

Eugen Paraschiv
4 years ago
Reply to  Felipe Borella

Hey Felipe – I can reproduce the problem – looking into it. Cheers,
Eugen.

Felipe Borella
Felipe Borella
4 years ago

I don’t know whats happends. The error persist.
There was an unexpected error (type=Forbidden, status=403).
Access Denied

Thanks

Eugen Paraschiv
4 years ago
Reply to  Felipe Borella

Hey Felipe – I can’t reproduce this one – I’m able to log in and consume the API just fine. If you have the exact steps to reproduce it – go ahead and open an issue on the github project and we’ll track it there. Thanks,
Eugen.

Arthur Carvalho
Arthur Carvalho
4 years ago
Reply to  Felipe Borella

Felipe, this error happened to me, you’re probably with the tomcat port other than 8081

Felipe Borella
Felipe Borella
4 years ago

Correct! Where are you from? Brazil?

Arthur Carvalho
Arthur Carvalho
4 years ago
Reply to  Felipe Borella

Brazil! 🙂

Felipe Borella
Felipe Borella
4 years ago

It works now! Thanks

maruf571
maruf571
4 years ago

It would be great if you provide some demo data with schema

Eugen Paraschiv
4 years ago
Reply to  maruf571

Well, there is an operation to create a new resource after login, so that’s a quick way to create some data. What other types of data would be help here? Cheers,
Eugen.

Johhny
Johhny
4 years ago

Hi Eugen,

What about acces_token refresh? What happens when the acces_token expires, in the password flow scenario?

Eugen Paraschiv
4 years ago
Reply to  Johhny

Hey Johny – so that depends on if the Resource Server is issuing refresh tokens (or not). If not – then you really don’t have any other option than go through the process again and get a new token. That’s of course going to inform your decision regarding the expiration period for these access tokens. But – if you do have refresh tokens – than you’ll use that to get a new access token. That way – access tokens can be very short-lived and it’s only the refresh token that is longer lived. Of course that also leads into things… Read more »

Johnny
Johnny
4 years ago

Well, that was the interesting question: how to store refresh tokens in order not to have security issues? It seems that I’ll have to wait until the Advanced API Security is written. Thanks.

Gaurav Dighe
Gaurav Dighe
4 years ago

Hi Eugene,

1. How to integrate User and Role saved in Database.
2. Here you have your HTML in the same project. How to integrate it with login page kept in different AngularJS project? Say my api at localhost:8080 and ui at localhost/login

Eugen Paraschiv
4 years ago
Reply to  Gaurav Dighe

Hey Gaurav,
1. Have a look at my registration tutorial for a working example – but very quickly, you’d simply use your own UserDetailsService
2. That’s a complex issue, so I won’t get into all of it, but you need to look into the CORS support in Spring to allow your JS to consume the API across origins and then you of course also need to make sure that your front end points to the correct URLs where your API is deployed.
Hope that helps. Cheers,
Eugen.

Comments are closed on this article!