Поток кода авторизации OAuth2 без разделения секрета клиента
Я сделал небольшую демонстрацию по потоку кода авторизации OAuth2, используя Spring Security Cloud с клиентом Angular 2.
Все работает нормально, я получаю ответ токена доступа от сервера.
Однако, согласно блогу Аарона Переки, https://aaronparecki.com/oauth-2-simplified/
Одностраничные приложения (или приложения на основе браузера) полностью загружаются в браузере после загрузки исходного кода с веб-страницы. Поскольку весь исходный код доступен для браузера, они не могут сохранять конфиденциальность своего клиентского секрета, поэтому в этом случае секрет не используется. Поток точно такой же, как и поток кода авторизации, описанный выше, но на последнем этапе код авторизации обменивается на токен доступа без использования секрета клиента.
Поэтому я не хочу использовать секрет клиента при получении токенов доступа с сервера аутентификации.
Однако я не могу продолжить работу, не передавая секрет клиента на сервер аутентификации.
Вот моя логика Angular 2 для получения токена
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;
}
}
Вот мой исходный код класса AuthorizationServerConfig
@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;
}
}
Исходный код для класса WebConfig
@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 класс
@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;
}
}
4 ответа
Я не видел реального вопроса там, но если все, что вам нужно, это скрыть Client Secret, почему бы вам не создать свой собственный API, вот где вы имеете дело со всеми вещами OAuth2, таким образом сохраняя свой Client Secret, ну секрет.
Это тот, кого вы вызываете из Angular.
Вы избегаете необходимости полностью раскрывать токен, вообще не используя его в JavaScript.
Да, это дополнительный шаг между ними, но он того стоит, если вы хотите что-то защитить.
Лучше всего вместо этого реализовать поток неявного предоставления. Рекомендуется для чисто фронтальных приложений. Вот хорошее прочтение о том, когда использовать какие (OAuth2) гранты и потоки OIDC https://community.apigee.com/articles/41719/when-to-use-which-oauth2-grants-and-oidc-flows.html
Поток кода авторизации, определенный в " 4.1. Предоставление кода авторизации" в RFC 6749, не требует client_secret
если тип клиента вашего приложения публичный.
Однако, даже если тип клиента вашего приложения общедоступен, вашему серверу авторизации требуется пара ключей API и API-секрета. Зачем? Это потому что WebSecurityConfig
защищает /oauth/**
, Защита будет выполнена, даже если /oauth/**
не были конечными точками OAuth.
(а) защита с помощью идентификатора клиента и тайны клиента и (б) защита общим способом (в данном случае защита WebSecurityConfig
) это разные вещи.
Чтобы добиться того, что вы описали, у вас есть 2 варианта:
- Используйте расширенный поток кода авторизации PKCE (SPA).
- Попросите сервер сохранить client_secret , который обменивает код авторизации на токен доступа. Вы можете использовать сервер, чтобы (1) вернуть токен доступа в SPA ИЛИ (2) использовать его для вызова цели (API и т. д.) от имени пользователя (предпочтительно, поскольку токен доступа не может быть перехвачен)