REST API using OAuth protection and use simple Angular client

1 Overview

In this tutorial, we'll use the REST API OAuth protection and use it from a simple Angular client .

We want to build applications that will contain four separate modules:
  • Authorization server
  • Resource server
  • UI implicit - front-end application using implicit stream
  • UI code - the front end of the application using the cipher stream

Before we begin - ** An important considerations. Keep in mind, Spring Security core team is implementing a new OAuth2 stack - Some aspects have been completed, some aspects are still in progress **.

This is a quick video, will provide you with some background about the work of information :
https://youtu.be/YI4YCJoOF0k

2. The authorization server

First, let's start the Authorization Server to a simple Spring Boot application.

2.1. Maven configuration

We will set the following dependencies set:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>    
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-jdbc</artifactId>
</dependency>  
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>org.springframework.security.oauth</groupId>
    <artifactId>spring-security-oauth2</artifactId>
</dependency>

Please note that we are using the spring-jdbc and MySQL, because we will use the token storage implementation JDBC support .

2.2。 @EnableAuthorizationServer

Now, let's start configuration is responsible for managing access token authorization server:

@Configuration
@EnableAuthorizationServer
public class AuthServerOAuth2Config
  extends AuthorizationServerConfigurerAdapter {
  
    @Autowired
    @Qualifier("authenticationManagerBean")
    private AuthenticationManager authenticationManager;
 
    @Override
    public void configure(
      AuthorizationServerSecurityConfigurer oauthServer) 
      throws Exception {
        oauthServer
          .tokenKeyAccess("permitAll()")
          .checkTokenAccess("isAuthenticated()");
    }
 
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) 
      throws Exception {
        clients.jdbc(dataSource())
          .withClient("sampleClientId")
          .authorizedGrantTypes("implicit")
          .scopes("read")
          .autoApprove(true)
          .and()
          .withClient("clientIdPassword")
          .secret("secret")
          .authorizedGrantTypes(
            "password","authorization_code", "refresh_token")
          .scopes("read");
    }
 
    @Override
    public void configure(
      AuthorizationServerEndpointsConfigurer endpoints) 
      throws Exception {
  
        endpoints
          .tokenStore(tokenStore())
          .authenticationManager(authenticationManager);
    }
 
    @Bean
    public TokenStore tokenStore() {
        return new JdbcTokenStore(dataSource());
    }
}

note:

  • For persistent token, we used JdbcTokenStore
  • We registered a customer "implicit" authorization type end
  • We signed up for another client -side and authorize the "password", "authorization_code" and "refresh_token" type of authorization
  • In order to use "password" authorization type, we need to connect and use AuthenticationManager bean

2.3. Data source configuration

Next, let's configure the data source JdbcTokenStore use:

@Value("classpath:schema.sql")
private Resource schemaScript;
 
@Bean
public DataSourceInitializer dataSourceInitializer(DataSource dataSource) {
    DataSourceInitializer initializer = new DataSourceInitializer();
    initializer.setDataSource(dataSource);
    initializer.setDatabasePopulator(databasePopulator());
    return initializer;
}
 
private DatabasePopulator databasePopulator() {
    ResourceDatabasePopulator populator = new ResourceDatabasePopulator();
    populator.addScript(schemaScript);
    return populator;
}
 
@Bean
public DataSource dataSource() {
    DriverManagerDataSource dataSource = new DriverManagerDataSource();
    dataSource.setDriverClassName(env.getProperty("jdbc.driverClassName"));
    dataSource.setUrl(env.getProperty("jdbc.url"));
    dataSource.setUsername(env.getProperty("jdbc.user"));
    dataSource.setPassword(env.getProperty("jdbc.pass"));
    return dataSource;
}

Please note that because we use JdbcTokenStore, we need to initialize the database schema, so we used DataSourceInitializer - as well as the SQL schema:

drop table if exists oauth_client_details;
create table oauth_client_details (
  client_id VARCHAR(255) PRIMARY KEY,
  resource_ids VARCHAR(255),
  client_secret VARCHAR(255),
  scope VARCHAR(255),
  authorized_grant_types VARCHAR(255),
  web_server_redirect_uri VARCHAR(255),
  authorities VARCHAR(255),
  access_token_validity INTEGER,
  refresh_token_validity INTEGER,
  additional_information VARCHAR(4096),
  autoapprove VARCHAR(255)
);
 
drop table if exists oauth_client_token;
create table oauth_client_token (
  token_id VARCHAR(255),
  token LONG VARBINARY,
  authentication_id VARCHAR(255) PRIMARY KEY,
  user_name VARCHAR(255),
  client_id VARCHAR(255)
);
 
drop table if exists oauth_access_token;
create table oauth_access_token (
  token_id VARCHAR(255),
  token LONG VARBINARY,
  authentication_id VARCHAR(255) PRIMARY KEY,
  user_name VARCHAR(255),
  client_id VARCHAR(255),
  authentication LONG VARBINARY,
  refresh_token VARCHAR(255)
);
 
drop table if exists oauth_refresh_token;
create table oauth_refresh_token (
  token_id VARCHAR(255),
  token LONG VARBINARY,
  authentication LONG VARBINARY
);
 
drop table if exists oauth_code;
create table oauth_code (
  code VARCHAR(255), authentication LONG VARBINARY
);
 
drop table if exists oauth_approvals;
create table oauth_approvals (
    userId VARCHAR(255),
    clientId VARCHAR(255),
    scope VARCHAR(255),
    status VARCHAR(10),
    expiresAt TIMESTAMP,
    lastModifiedAt TIMESTAMP
);
 
drop table if exists ClientDetails;
create table ClientDetails (
  appId VARCHAR(255) PRIMARY KEY,
  resourceIds VARCHAR(255),
  appSecret VARCHAR(255),
  scope VARCHAR(255),
  grantTypes VARCHAR(255),
  redirectUrl VARCHAR(255),
  authorities VARCHAR(255),
  access_token_validity INTEGER,
  refresh_token_validity INTEGER,
  additionalInformation VARCHAR(4096),
  autoApproveScopes VARCHAR(255)
);

Please note that we differ given the need to explicitly DatabasePopulator bean - that we can simply use schema.sql - Spring Boot use it by default .

2.4. Security Configuration

Finally, let us protect authorization server.

When the client should be needed to obtain an access token program, which will be performed after the authentication process in a simple form login-driven :

@Configuration
public class ServerSecurityConfig extends WebSecurityConfigurerAdapter {
 
    @Override
    protected void configure(AuthenticationManagerBuilder auth) 
      throws Exception {
        auth.inMemoryAuthentication()
          .withUser("john").password("123").roles("USER");
    }
 
    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() 
      throws Exception {
        return super.authenticationManagerBean();
    }
 
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
            .antMatchers("/login").permitAll()
            .anyRequest().authenticated()
            .and()
            .formLogin().permitAll();
    }
}

Here's a simple explanation is the password to log stream does not need to form configuration - only for implicit flow - so you can based on your positive in the OAuth2 flow used to skip it.

3. Resource Server

Now, let's discuss the resource server; we eventually want to be able to use the REST API this nature.

3.1. Maven configuration

Our resource server configuration with the previous authorization server configured with the same application.

3.2. Token storage configuration

Next, we will configure TokenStore authorization server to access the same database for storage access token:

@Autowired
private Environment env;
 
@Bean
public DataSource dataSource() {
    DriverManagerDataSource dataSource = new DriverManagerDataSource();
    dataSource.setDriverClassName(env.getProperty("jdbc.driverClassName"));
    dataSource.setUrl(env.getProperty("jdbc.url"));
    dataSource.setUsername(env.getProperty("jdbc.user"));
    dataSource.setPassword(env.getProperty("jdbc.pass"));
    return dataSource;
}
 
@Bean
public TokenStore tokenStore() {
    return new JdbcTokenStore(dataSource());
}

Please note that for this simple implementation, we share the stored tokens SQL support, authority and resources even if the server is a separate application .

Of course, because the resource server needs to be able to check the validity of the authorization server access token issued.

3.3. Remote Token Service

We can use RemoteTokeServices instead of using TokenStore in the Resource Server:

@Primary
@Bean
public RemoteTokenServices tokenService() {
    RemoteTokenServices tokenService = new RemoteTokenServices();
    tokenService.setCheckTokenEndpointUrl(
      "http://localhost:8080/spring-security-oauth-server/oauth/check_token");
    tokenService.setClientId("fooClientIdPassword");
    tokenService.setClientSecret("secret");
    return tokenService;
}
note:
  • This RemoteTokenService will use CheckTokenEndPoint on the authorization server to verify and obtain AccessToken Authentication object.
  • Can be found in AuthorizationServerBaseURL + "/ oauth / check_token"
  • Authorization Server can use any type TokenStore [JdbcTokenStore, JwtTokenStore, ...] - This will not affect RemoteTokenService or Resource Server.

3.4. Controller Sample

Next, let's implement a simple controller public Foo resources:

@Controller
public class FooController {
 
    @PreAuthorize("#oauth2.hasScope('read')")
    @RequestMapping(method = RequestMethod.GET, value = "/foos/{id}")
    @ResponseBody
    public Foo findById(@PathVariable long id) {
        return
          new Foo(Long.parseLong(randomNumeric(2)), randomAlphabetic(4));
    }
}

Notice how the client side needs to "read" scope to access this resource.

We also need to enable global security and configure the method MethodSecurityExpressionHandler :

@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class OAuth2ResourceServerConfig 
  extends GlobalMethodSecurityConfiguration {
 
    @Override
    protected MethodSecurityExpressionHandler createExpressionHandler() {
        return new OAuth2MethodSecurityExpressionHandler();
    }
}

This is our basic Foo resources :

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

3.5. Web Configuration

Finally, let's set a very basic configuration for the Web API:

@Configuration
@EnableWebMvc
@ComponentScan({ "org.baeldung.web.controller" })
public class ResourceWebConfig implements WebMvcConfigurer {}

4. Front End - Set

We will now see a simple front-end Angular client implementation.

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

First, we will install node and npm - because Angular CLI is a npm tool .

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

<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>

Finally, Angular CLI generates a new module:

ng new oauthApp

Please note that we will have two front-end modules - one for the cipher stream, and another for implicit flow .

In the following sections, we will discuss Angular app logic for each module .

5. Angular password stream

We will use the password OAuth2 flow here - that's why this is just a proof of concept, rather than a production-ready application program. You will notice that the client credentials have been exposed to the front - this is what we'll cover in a future article.

Our use case is simple: Once a user provides their credentials, front-end client will use them to obtain access token from the authorization server .

5.1. Application Services

Let's located app.service.ts AppService start - it contains the logical server interaction:

  • obtainAccessToken (): Get a given user credentials token Access
  • saveToken (): Use ng2-cookies library access token is stored in a cookie
  • the getResource (): Foo object using its ID acquired from the server
  • checkCredentials (): Check whether the user is logged in
  • Zimbabwe Logout (): Delete access token cookie and the user logs off
export class Foo {
  constructor(
    public id: number,
    public name: string) { }
} 
 
@Injectable()
export class AppService {
  constructor(
    private _router: Router, private _http: Http){}
  
  obtainAccessToken(loginData){
    let params = new URLSearchParams();
    params.append('username',loginData.username);
    params.append('password',loginData.password);    
    params.append('grant_type','password');
    params.append('client_id','fooClientIdPassword');
    let headers = new Headers({'Content-type': 'application/x-www-form-urlencoded; charset=utf-8',
      'Authorization': 'Basic '+btoa("fooClientIdPassword:secret")});
    let options = new RequestOptions({ headers: headers });
     
    this._http.post('http://localhost:8081/spring-security-oauth-server/oauth/token', 
      params.toString(), options)
      .map(res => res.json())
      .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);
    this._router.navigate(['/']);
  }
 
  getResource(resourceUrl) : Observable<Foo>{
    var headers = new Headers({'Content-type': 'application/x-www-form-urlencoded; charset=utf-8',
      'Authorization': 'Bearer '+Cookie.get('access_token')});
    var options = new RequestOptions({ headers: headers });
    return this._http.get(resourceUrl, options)
                   .map((res:Response) => res.json())
                   .catch((error:any) => Observable.throw(error.json().error || 'Server error'));
  }
 
  checkCredentials(){
    if (!Cookie.check('access_token')){
        this._router.navigate(['/login']);
    }
  } 
 
  logout() {
    Cookie.delete('access_token');
    this._router.navigate(['/login']);
  }
}
note:
  • To get the access token, we will send POST to "/ oauth / token" endpoints
  • We use client credentials and Basic Auth to hit this endpoint
  • We will then send the user credentials and the client ID and URL encoding parameters granted type
  • After obtaining the access token - we will store it in a cookie

Stored in a cookie here is especially important, because we just cookie used for storage purposes rather than directly drive the authentication process . This helps prevent cross-site request forgery (CSRF) types of attacks and vulnerabilities.

5.2. Login Components

Next, let's look at LoginComponent responsible for the login form:

@Component({
  selector: 'login-form',
  providers: [AppService],  
  template: `<h1>Login</h1>
    <input type="text" [(ngModel)]="loginData.username" />
    <input type="password"  [(ngModel)]="loginData.password"/>
    <button (click)="login()" type="submit">Login</button>`
})
export class LoginComponent {
    public loginData = {username: "", password: ""};
 
    constructor(private _service:AppService) {}
  
    login() {
        this._service.obtainAccessToken(this.loginData);
    }

5.3. Home Components

Next, our HomeComponent responsible for displaying and operating our homepage:

@Component({
    selector: 'home-header',
    providers: [AppService],
  template: `<span>Welcome !!</span>
    <a (click)="logout()" href="#">Logout</a>
    <foo-details></foo-details>`
})
  
export class HomeComponent {
    constructor(
        private _service:AppService){}
  
    ngOnInit(){
        this._service.checkCredentials();
    }
  
    logout() {
        this._service.logout();
    }
}

5.4. Foo Components

Finally, we show our FooComponent Foo details:

@Component({
  selector: 'foo-details',
  providers: [AppService],  
  template: `<h1>Foo Details</h1>
    <label>ID</label> <span>{{foo.id}}</span>
    <label>Name</label> <span>{{foo.name}}</span>
    <button (click)="getFoo()" type="submit">New Foo</button>`
})
 
export class FooComponent {
    public foo = new Foo(1,'sample foo');
    private foosUrl = 'http://localhost:8082/spring-security-oauth-resource/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. Application Components

Our simple AppComponent act as the root component:

@Component({
    selector: 'app-root',
    template: `<router-outlet></router-outlet>`
})
 
export class AppComponent {}

And we all packaging components, services and routing AppModule:

@NgModule({
  declarations: [
    AppComponent,
    HomeComponent,
    LoginComponent,
    FooComponent    
  ],
  imports: [
    BrowserModule,
    FormsModule,
    HttpModule,
    RouterModule.forRoot([
     { path: '', component: HomeComponent },
    { path: 'login', component: LoginComponent }])
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

6. implicit flow

Next, we will focus on Implicit Flow module.

6.1. Application Services

Similarly, we will begin our service, but this time we will use the library angular-oauth2-oidc rather than trying to get an access token:

@Injectable()
export class AppService {
  
  constructor(
    private _router: Router, private _http: Http, private oauthService: OAuthService){
        this.oauthService.loginUrl = 'http://localhost:8081/spring-security-oauth-server/oauth/authorize'; 
        this.oauthService.redirectUri = 'http://localhost:8086/';
        this.oauthService.clientId = "sampleClientId";
        this.oauthService.scope = "read write foo bar";    
        this.oauthService.setStorage(sessionStorage);
        this.oauthService.tryLogin({});      
    }
  
  obtainAccessToken(){
      this.oauthService.initImplicitFlow();
  }
 
  getResource(resourceUrl) : Observable<Foo>{
    var headers = new Headers({'Content-type': 'application/x-www-form-urlencoded; charset=utf-8',
     'Authorization': 'Bearer '+this.oauthService.getAccessToken()});
    var options = new RequestOptions({ headers: headers });
    return this._http.get(resourceUrl, options)
      .map((res:Response) => res.json())
      .catch((error:any) => Observable.throw(error.json().error || 'Server error'));
  }
 
  isLoggedIn(){
    if (this.oauthService.getAccessToken() === null){
       return false;
    }
    return true;
  } 
 
  logout() {
      this.oauthService.logOut();
      location.reload();
  }
}

Please note that after gaining access token, every time we use a protected resource from the resource server, we will use it through the Authorization header .

6.2. Home Components

Our HomeComponent deal with our simple home page:

@Component({
    selector: 'home-header',
    providers: [AppService],
  template: `
    <button *ngIf="!isLoggedIn" (click)="login()" type="submit">Login</button>
    <div *ngIf="isLoggedIn">
        <span>Welcome !!</span>
        <a (click)="logout()" href="#">Logout</a>
        <br/>
        <foo-details></foo-details>
    </div>`
})
  
export class HomeComponent {
    public isLoggedIn = false;
 
    constructor(
        private _service:AppService){}
     
    ngOnInit(){
        this.isLoggedIn = this._service.isLoggedIn();
    }
 
    login() {
        this._service.obtainAccessToken();
    }
 
    logout() {
        this._service.logout();
    }
}

6.3. Foo Components

Our FooComponent exactly the same cipher stream module.

6.4. Application Module

Finally, our AppModule:

@NgModule({
  declarations: [
    AppComponent,
    HomeComponent,
    FooComponent    
  ],
  imports: [
    BrowserModule,
    FormsModule,
    HttpModule,
    OAuthModule.forRoot(),    
    RouterModule.forRoot([
     { path: '', component: HomeComponent }])
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

7. Run the tip

1. To run any of our front-end module, we need to build the application:
mvn clean install
2. We then need to navigate to our Angular app catalog:
cd src/main/resources
3. Finally, we will start our application:
npm start

By default, the server will start on port 4200, the port module to change any change

"start": "ng serve"

It runs on port 8086 in package.json, for example:

"start": "ng serve --port 8086"

8. Conclusion

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

You can be found in this tutorial GitHub project fully implemented.

Guess you like

Origin www.cnblogs.com/xjknight/p/10959614.html