OAuth2 i Spring Boot
Sadržaj |
Uvod
U ovom radu bit će objašnjen princip rada OAuth protokola te će ukratko biti razjašnjeno što je to Spring Boot. Bit će objašnjeno koje sve role postoje u OAuth protokolu, vrste token storea, grant typeova i slično. Na kraju će biti pokazano kako pomoću Spring Boota ostvariti OAuth2 standard, točnije i Authorization server i Resource server.
OAuth
OAuth je autorizacijski standard koji omogućava klijentskim aplikacijama pristup korisničkim podacima na HTTP servisima kao što su Facebook, Google, Twiter itd. Bez dijeljenja korisnikove lozinke. Na primjer, ako imate aplikaciju koja za korisnika može objavljivati postove na Facebooku, korisnik se prijavljuje na Facebook svojim korisničkim imenom i lozinkom te na taj način autorizira aplikaciju da u njegovo ime smije objavljivati postove. Jednom kad se korisnik prijavio aplikacija dobiva access token te se na taj način autorizira bez da koristi i zna korisničke podatke.
Nastao je krajem 2006. godine, a krajem 2012. objavljena je nova verzija nazvana Oauth 2.0 ili kraće OAuth2. Kod OAuth2 je pojednostavljen razvoj klijentskih aplikacija jer pruža posebne autorizacijske tokove za web aplikacije, desktop aplikacije i mobilne telefone.
Role
Kod OAuth standarda je ključno raspoznavati 4 role:
- Resource owner – korisnik koji autorizira klijentsku aplikaciju (Client) tj. Dozvoljava joj pristup podacima.
- Authorization server – autenticira korisnika (Resource ownera) te dodijeljuje access token klijetskoj aplikaciji (Client)
- Resource server (API) – server s podacima kojima se želi pristupiti
- Client – klijentska aplikacija koja želi pristupiti korisničkim podacima.
Registracija aplikacije - Client ID i Client Secret
Da bi bilo moguće koristiti autentikaciju i autorizacijeu preko neke treće strane potrebno je prvo registrirati svoju aplikaciju na tom servisu. Kod registracije je potrebno popuniti informacije kao što su ime aplikacije, web adresa aplikacije i redirect ili callback URL. Nakon registracije bit će dodijeljeni client ID i client secret. Client id je javni string koji služi za identifikaciju aplikacije i sastavljanje autorizacijskog URL-a. Client ID služi kako bi servis autenticirao aplikaciju i naravno mora biti privatan.
U našem primjeru registracija neće biti potrebna jer će u konfiguraciji autorizacijskog servera biti postavljeni client id i secret koji će se koristiti.
Tokeni
Tokeni se dobivaju o strane autorizacijskogservera naon što su autentikacija i utorizacija prošli.
Postoje dvije vrste tokena:
- Access token - Token koji aplikacija šalje kako bi mogla pristupiti resursima. Ima graničen, kratak vijek trajanja što napadaču daje kratak rok da ga iskoristi ako je slučajno neovlašteno došao do istog. Postoje Bearer i MAC access tokeni. Bearer se koristi češće i kod jega se jednostavno dobiva čisti access token u obliku stringa. Kod MACa se token sprema u Key Manager kao secret te se aplikaciji šalje kriptiran.
- Refresh token - Služi za dobavljanje novog access tokena. Ukoliko netko i neovlašteno dođe do refresh tokena, ne može napraviti ništa jer se uz njega moraju poslati i client ID i client secret kako bi se dobio novi access token.
Grant Types
Postoje četiri načina traženja pristupa te se koriste ovisno o tipovima koje podržava servis API te vrsti aplikacije koja traži pristup.
Tipovi traženja pristup su:
- Authorization Code - Koristi se kod server-side aplikacija gdje source kod nije dostupan javno i gdje se može garantirati tajnost Client secreta. Bazira se na redirect URL-u te je najkorišteniji. Access token se šalje direktno aplikaciji.
- Implicit - Koristi se kod mobilnih i web aplikacija, tj. aplikacijama koje se pokreću na korisnikovom uređaju te se ne može garantirati povjerljivost Client secreta. Bazira se isto a redirect URL-u, ali se kod njega access token ne šalje aplikaciji već user agent (npr. browseru) dobiva skriptu iz koje može izvući access token i proslijediti ga aplikaciji. Ovdje se ne provjerava aplikacija koja pokušava pristupiti servisu (ne šalje se client secret). Također, refresh token ovdje nije podržan.
- Resource Owner Password Credentials - Korisnik upisuje username i password direktno u aplikaciju te ih aplikacija koristi za dobivanje access tokena. Nije preporučeno koristiti ovaj način osim kod aplikacija kojima vjerujemo ili ako baš ni jedan drugi način nije moguć.
- Client Credentials - Korisiti se kad aplikacija želi pristupiti svom vlastitom servisnom raučunu i promijeniti npr. opis registracije ili redirect URL ili dohvatiti neke podatke svog računa putem API-ja.
Spring Boot
Spring Boot znatno olakšava izradu aplikacija koje koriste Spring tako da na temelju startera koji se uključe u pom automatski pretpostavlja koji su sve jar-ovi potrebni te na temelju toga i anotacija slaže konfiguraciju. Npr. Ako želimo napraviti web aplikaciju u pom je potrebno uključiti “spring-boot-starter-web” te će Spring Boot uključiti apsolutno sve što je potrebno za razvoj full-stack web aplikacije pa čak i Tomcat i spring-webmvc. Također omogućava lagano mijenjanje ili podešavanje nekih postavki kroz application.properties s već unaprijed određenim ključevima i mogućim vrijednostima. Ako za nešto želimo ručno napisati konfiguraciju i to je naravno moguće uz određenu anotaciju.
Konfiguracija
Kao server bit će korišten Apache Tomcat. Biti će konfigururan da koristi ssl, tj. HTTPS, te će i Spring Boot aplikacija biti konfigurirana da prihvaća samo HTTPS zahtjeve.
Tomcat
Kako bi omogućili ssl na Tomcatu, potrebno je izgenerirati certfikat pozivanjem naredbe:
$JAVA_HOME/bin/keytool -genkey -alias tomcat -keyalg RSA -keystore /path/to/my/keystore
Nakon toga potrebno je u server.xml datoteci odkomentirati sljedeći dio
<Connector port="8443" protocol="org.apache.coyote.http11.Http11NioProtocol" maxThreads="150" SSLEnabled="true" scheme="https" secure="true" keystoreFile="" keystorePass="" clientAuth="false" sslProtocol="TLS" />
To će omogućiti ssl na portu 8443. Pod keystoreFile potrebno je dodati putanju do keystorea, a pod keystorePass lozinku za taj keystore.
Authorization i Resource server
Ovaj tutorial bit će orijentiran na Oauth pa neće biti prikazane klase kao što su hibernate modeli ili interface i implementacije servisnog ili DAO sloja.
Preko web stranice https://start.spring.io/ ili Eclipse dodatka moguće je izgenerirati projekt koji će automatski postaviti sve što treba za početak te je aplikaciju odmah moguće pokrenuti.
Klasa u kojoj je konfiguracija OAuth Authorization servera i Resource servera izgleda ovako:
package hr.optimit.security; import javax.sql.DataSource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.bind.RelaxedPropertyResolver; import org.springframework.context.EnvironmentAware; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.env.Environment; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer; import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter; import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer; import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter; import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer; import org.springframework.security.oauth2.provider.token.TokenStore; import org.springframework.security.oauth2.provider.token.store.JdbcTokenStore; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; @Configuration public class OAuth2Configuration { @Configuration @EnableResourceServer protected static class ResourceServerConfiguration extends ResourceServerConfigurerAdapter { @Autowired private CustomAuthenticationEntryPoint customAuthenticationEntryPoint; @Autowired private CustomLogoutSuccessHandler customLogoutSuccessHandler; @Override public void configure(HttpSecurity http) throws Exception { http .exceptionHandling().authenticationEntryPoint(customAuthenticationEntryPoint) .and() .logout() .logoutUrl("/oauth/logout") .logoutSuccessHandler(customLogoutSuccessHandler) .and() .csrf() .requireCsrfProtectionMatcher(new AntPathRequestMatcher("/oauth/authorize")) .disable() .headers() .frameOptions().disable() .and() .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .requiresChannel() .anyRequest().requiresSecure() .and() .authorizeRequests() .antMatchers("/mt2a/api/**").authenticated(); } } @Configuration @EnableAuthorizationServer protected static class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter implements EnvironmentAware { private static final String ENV_OAUTH = "authentication.oauth."; private static final String PROP_CLIENTID = "clientid"; private static final String PROP_SECRET = "secret"; private static final String PROP_TOKEN_VALIDITY_SECONDS = "tokenValidityInSeconds"; private RelaxedPropertyResolver propertyResolver; @Autowired private DataSource dataSource; @Bean public TokenStore tokenStore() { return new JdbcTokenStore(dataSource); } @Autowired @Qualifier("authenticationManagerBean") private AuthenticationManager authenticationManager; @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { endpoints .tokenStore(tokenStore()) .authenticationManager(authenticationManager); } @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients .inMemory() .withClient(propertyResolver.getProperty(PROP_CLIENTID)) .scopes("read", "write") .authorizedGrantTypes("password", "refresh_token") .secret(propertyResolver.getProperty(PROP_SECRET)) .accessTokenValiditySeconds( propertyResolver.getProperty(PROP_TOKEN_VALIDITY_SECONDS, Integer.class, PROP_TOKEN_VALIDITY_SECONDS)); } @Override public void setEnvironment(Environment environment) { this.propertyResolver = new RelaxedPropertyResolver(environment, ENV_OAUTH); } } }
Application.properties datoteka sadrži sljedeće vrijednosti
#data source properties spring.datasource.url=jdbc:postgresql://localhost:5432/mt2a spring.datasource.username=postgres spring.datasource.password=Zagy1991 # JPA properties spring.jpa.show_sql=true spring.jpa.generate-ddl=false spring.jpa.hibernate.ddl-auto=update spring.data.jpa.repositories.enabled=true spring.data.rest.base-path=/mt2a/api #Application specific authentication.oauth.clientid=mt2a authentication.oauth.secret=Zagy1991 authentication.oauth.tokenValidityInSeconds=1800 security.require_ssl=true
Kod konfigracije Resource servera koištene su neke standardnespring postavke kao što su rukovanje iznimkama kod logina, logout url i success handler, zaštita od CSRF-a i slino. Najvažnije su zapravo zadnjedvije linije koje kažu da svaki zahjev koji dolazi na url /mt2/api/ mora biti autenticiran.
Kod Authorization servera je postavljn authentication manager i Jdbc Token Store. Kod Jdbc Token Storea se tokeni spremaju u bazu podataka. Postoje još i In Memory Token Store koji sprema tokene u memorijuili JWT Token store koji ih zapravo ne sprema nigdje već samo čita informacije iz tokena.
Da bi se mogao koristiti Jdbc Token store potrebno je kreirati određene tablice u bazi. Sljedeće skripte služe za kreiranje tih tablica na Postgres bazi.
create table oauth_access_token ( token_id VARCHAR(256), token bytea, authentication_id VARCHAR(256), user_name VARCHAR(256), client_id VARCHAR(256), authentication bytea, refresh_token VARCHAR(256) ); create table oauth_refresh_token ( token_id VARCHAR(256), token bytea, authentication bytea );
Grant Type je postavljen na password i na refresh token. Client ID, client secret i trajanje access tokena se čitaju iz application.properties datoteke.
Testiranje
Za dobivanje access tokena poslan je sljedeći zahtjev:
curl -k -vu mt2a:Zagy1991 -d "username=hlevnjak&password=test&grant_type=password" https://localhost:8443/oauth/token
U odgovoru su dobiveni access i refresh token.
{"access_token":"2cacdb78-b7f2-4d14-a518-75c68cca8881","token_type":"bearer","refresh_token":"361a6e1b-1864-4f10-8433-744447fd8d98","expires_in":59,"scope":"read write"}
Nakon toga je iskorišten access token kako bi dohvatili podatke. Poslan je sljedeći zahtjev:
curl -i -k -H "Authorization: Bearer 2cacdb78-b7f2-4d14-a518-75c68cca8881" https://localhost:8443/mt2a/api/users <pre> Dobiven je sljedeći odgovor: <pre> { "_embedded" : { "users" : [ { "userUsername" : "hlevnjak", "userPassword" : "57900f52882ff73115d43a5cfb82b1fcf4730ff2dd5ab8746c7a663887cb605a3fa8ecab41ad22b7", "userStatus" : "active", "userRole" : "user", "userFirstName" : "Tomislav", "userLastName" : "Hlevnjak", "userEmail" : "hlevnjak.tomislav@gmail.com", "userCreationTimestamp" : "2016-02-25T17:49:38.640+0000", "sysModificationTimestamp" : "2016-02-25T17:49:38.640+0000", "sysModificationUser" : 1, "allowedDomains" : "*", "_links" : { "self" : { "href" : "https://localhost:8443/mt2a/api/users/1" }, "apnaUser" : { "href" : "https://localhost:8443/mt2a/api/users/1" } } } ] }, "_links" : { "self" : { "href" : "https://localhost:8443/mt2a/api/users" }, "profile" : { "href" : "https://localhost:8443/mt2a/api/profile/users" }, "search" : { "href" : "https://localhost:8443/mt2a/api/users/search" } } }
Kada istekne access token,moguće ga je ponovno zatražiti pomoću refresh tokena.
curl -k -vu mt2a:Zagy1991 -d "username=hlevnjak&password=test&grant_type=refresh_token&refresh_token=361a6e1b-1864-4f10-8433-744447fd8d98" http://localhost:8080/oauth/token
Zaključak
OAuth je odličan i trenutno jako korišten način autentikacije i autorizacije klijentsikh aplikacija sa HTTP servisima kao što su Facebook, Twitter, Google itd. Obzirom da svi večina takvih servisa ima omogućen OAuth, nije komplicirana implementacija istog na klijentskim aplikacijama bilo da se radi o web aplikacijama, desktop aplikacijama ili mobilim aplikacijama. Uz Sprng Boot implementacija Autorizacijskog i Resurce servera je također jednostavna što je i prikazano ranije.