OAuth2 Authorization Code flow without sharing client secret

7.2k Views Asked by At

I have made a small demo on Authorization Code flow of OAuth2 using Spring Security Cloud with Angular 2 client.

Everything is working fine, i am getting the access token response from server.

However as per Aaron perecki's blog https://aaronparecki.com/oauth-2-simplified/

Single-page apps (or browser-based apps) run entirely in the browser after loading the source code from a web page. Since the entire source code is available to the browser, they cannot maintain the confidentiality of their client secret, so the secret is not used in this case. The flow is exactly the same as the authorization code flow above, but at the last step, the authorization code is exchanged for an access token without using the client secret.

So I don't want to use client secret in getting access tokens from auth-server.

However, I am not able to proceed without sharing client secret to auth server.

The following is my Angular 2 logic to retrieve token

import {Injectable} from '@angular/core';
import {IUser} from './user';
import {Router} from '@angular/router';
import {Http, RequestOptions, Headers, URLSearchParams} from '@angular/http';

@Injectable()
export class AuthService {
  currentUser: IUser;
  redirectUrl: string;
  state: string;
  tokenObj: any;

  constructor(private router: Router, private http: Http) {
    this.state = '43a5';
  }

  isLoggedIn(): boolean {
    return !!this.currentUser;
  }

  loginAttempt(username: string, password: string): void {
    const credentials: IUser = {
      username: username,
      password: password
    };

    const params = new URLSearchParams();
    params.append('client_id', 'webapp');
    params.append('redirect_uri', 'http://localhost:9090/callback');
    params.append('scope', 'read');
    params.append('grant_type', 'authorization_code');
    params.append('state', this.state);
    params.append('response_type', 'code');

    const headers = new Headers({
      'Authorization': 'Basic ' + btoa(username + ':' + password)
    });

    const options = new RequestOptions({headers: headers});

    this.http.post('http://localhost:9090/oauth/authorize', params, options)
      .subscribe(
        data => {
          const authresponse = data.json();
          this.tokenObj = this.getTokens(authresponse.code).json();
        },
        err => console.log(err)
      );
  }

  getTokens(code: string): any {
    const params = new URLSearchParams();
    params.append('grant_type', 'authorization_code');
    params.append('code', code);
    params.append('redirect_uri', 'http://localhost:9090/callback');

    const headers = new Headers({
      'Authorization': 'Basic ' + btoa('webapp:websecret')
    });

    const options = new RequestOptions({headers: headers});

    this.http.post('http://localhost:9090/oauth/token', params, options)
      .subscribe(
        data => {
          return data.json();
        },
        err => console.log(err)
      );
  }

  logout(): void {
    this.currentUser = null;
  }
}

Here is my AuthorizationServerConfig class source code

@Configuration
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private AuthenticationManager authManager;

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.accessTokenConverter(accessTokenConverter()).authenticationManager(authManager);
    }

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security.checkTokenAccess("permitAll()");
    }

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.jdbc(dataSource());
    }

    @Bean
    public TokenStore tokenStore() {
        return new JwtTokenStore(accessTokenConverter());
    }

    @Bean
    public DataSource dataSource() {
        DriverManagerDataSource dataSource = new DriverManagerDataSource();
        dataSource.setDriverClassName("com.mysql.jdbc.Driver");
        dataSource.setUrl("jdbc:mysql://localhost:3306/oauth2?createDatabaseIfNotExist=true");
        dataSource.setUsername("root");
        dataSource.setPassword("chandra");
        return dataSource;
    }

    @Bean
    public JwtAccessTokenConverter accessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setSigningKey("123");
        return converter;
    }

    @Bean
    @Primary
    public DefaultTokenServices tokenServices() {
        DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
        defaultTokenServices.setTokenStore(tokenStore());
        defaultTokenServices.setSupportRefreshToken(true);
        return defaultTokenServices;
    }
}

Source code for WebConfig class

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication().withUser("user1").password("password1").roles("USER")
            .and().withUser("admin1").password("password1").roles("ADMIN");
        auth.eraseCredentials(false);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
            .antMatchers("/oauth/**").permitAll()
            .anyRequest().authenticated()
            .and().csrf().disable()
            .httpBasic();
    }
}

SpringBootApplication class

@SpringBootApplication
@EnableAuthorizationServer
@RestController
public class SpringMicroservicesOauthServerApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringMicroservicesOauthServerApplication.class, args);
    }

    @RequestMapping("callback")
    public AuthCodeResponse test(@RequestParam("code") String code, @RequestParam("state") String state) {
        return new AuthCodeResponse(code,state);
    }
}

AuthCodeResponse POJO

public class AuthCodeResponse {

    private String code;

    private String state;

    public AuthCodeResponse() {
    }

    public AuthCodeResponse(String code, String state) {
        this.code = code;
        this.state = state;
    }

    public String getCode() {
        return code;
    }

    public void setCode(String code) {
        this.code = code;
    }

    public String getState() {
        return state;
    }

    public void setState(String state) {
        this.state = state;
    }
}
3

There are 3 best solutions below

3
On

I haven't seen an actual question in there but if all you want is to hide the Client Secret, why don't you create an API of your own, that's where you deal with all the OAuth2 stuff, thus keeping you Client Secret, well secret.

That's the one you call from the Angular front-end.

You avoid the need for exposing the token completely by not using it in JavaScript at all.

Yes, it's an extra step in between but totally worth it if you want to keep anything secure.

0
On

The authorization code flow defined in "4.1. Authorization Code Grant" in RFC 6749 does not require client_secret if the client type of your application is public.

However, even if the client type of your application is public, your authorization server requires a pair of API key and API secret. Why? It's because WebSecurityConfig is protecting /oauth/**. The protection would be performed even if /oauth/** were not OAuth endpoints.

(a) Protection by Client ID and Client secret and (b) protection by a generic way (in this case, the protection by WebSecurityConfig) are different things.

0
On

To achieve what you described, you have 2 options:

  • Use the PKCE-enhanced authorization code flow (SPA)
  • Have a server store the client_secret that exchanges the authorization code for an access token. You can use the server to either (1) return the access token to the SPA OR (2) use it to call the target (API etc.) on the user's behalf (preferred, as the access token can't be intercepted)