29. ledna 2018

Spring Security, SAML & ADFS: Implementace

Posledně jsme se vyřádili na konfiguraci, tak teď už jen zbývá to nabouchat v tom Springu, ne? Dobrá zpráva je, že pokud budete následovat Reference Documentation, bude vám Spring SAML a ADFS fungovat out-of-the-box.

Špatná zpráva je, že pokud budete chtít použít Java configuration, nemáte se moc kde inspirovat. Pokud vím, tak k dnešnímu dni jsou k dispozici jen dva příklady:
Dalším benefitem mého příspěvku a ukázkového projektu je, že používají aktuální verzi Spring Frameworku a Spring Security, tedy verzi 5 (v tomhle asi budu chviličku unikátní). Třešničkou na dortu je pak buildování pomocí Gradle (protože kdo by ještě chtěl v dnešní době používat Maven, že jo? ;-)

Závislosti

Pro zdar operace budeme potřebovat následující závislosti:

ext {
springVersion = '5.0.2.RELEASE'
springSecurityVersion = '5.0.0.RELEASE'
springSamlVersion = '1.0.3.RELEASE'
}
dependencies {
implementation "org.springframework:spring-webmvc:${springVersion}"
implementation "org.springframework.security:spring-security-web:${springSecurityVersion}"
implementation "org.springframework.security:spring-security-config:${springSecurityVersion}"
implementation "org.springframework.security.extensions:spring-security-saml2-core:${springSamlVersion}"
}
Drobná Gradle poznámka: Protože používám současnou verzi Gradlu, používám konfiguraci implementation. Pro starší verze Gradle (2.14.1-) použijte původní (nyní deprecated) konfiguraci compile.

Spring SAML Configuration

Ať už se použije XML, nebo Java konfigurace, bude to v každém případě velmi dlouhý soubor. Velmi. I když nebudu počítat téměř 40 řádek importů, i tak zabere ta nejzákladnější konfigurace zhruba 5 obrazovek. Víc se mi to ořezat nepodařilo.

Ale nebojte se, nebudu vás oblažovat každým detailem. Jen vypíchnu to zajímavé, vynechám co jsem zmiňoval v minulém díle o konfiguraci a pro zbytek konfigurace vás odkážu do svého repozitáře, kde si to můžete vychutnat celé: SecurityConfiguration.java.

Nastavení HttpSecurity

Nebudu příliš zabíhat do podrobností, jak funguje samotné Spring Security (prostě chrání pomocí filtrů určité URL/zdroje) a podívám se na jedno konkrétní nastavení:

@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf()
.disable();
http
.addFilterBefore(metadataGeneratorFilter(), ChannelProcessingFilter.class)
.addFilterAfter(samlFilter(), BasicAuthenticationFilter.class);
http.authorizeRequests()
.antMatchers("/")
.permitAll()
.antMatchers("/user")
.authenticated().and()
.formLogin()
.loginPage("/saml/login");
http.logout()
.logoutSuccessUrl("/");
}
}
Uvedené nastavení definuje:
  • Vypnuté CSRF. U SAMLu nedává CSRF moc smysl - SAML requesty jsou podepsané privátním klíčem daného SP, jehož veřejný klíč je zaregistrován na použitém IdP.
  • Přídání dvou filtrů: jeden pro SAML metadata (metadataGeneratorFilter), druhý řeší samotný SAML mechanismus (samlFilter).
  • Definice URL, které vyžadují autentikaci (/user). 
  • Podstrčení SAML entry pointu namísto přihlašovacího formuláře (loginPage("/saml/login")).
  • Přesměrování na root kontext aplikace po úspěšném odhlášení (logoutSuccessUrl("/")).

SAML filtry

Základem jak Spring Security, tak Spring Security SAMLu jsou filtry - odchytí HTTP(S) komunikaci a transparentně aplikují zabezpečení aplikace. V případě SAMLu je těch filtrů celá smečka, ale v zásadě řeší jen tři věci: přihlášení (SSO), odhlášení (SLO) a metadata. Čtvrtým mušketýrem může být ještě IdP discovery, ale tu v našem případě nemáme.

@Bean
public FilterChainProxy samlFilter() throws Exception {
List<SecurityFilterChain> chains = new ArrayList<SecurityFilterChain>();
chains.add(new DefaultSecurityFilterChain(
new AntPathRequestMatcher("/saml/login/**"),
samlEntryPoint()));
chains.add(new DefaultSecurityFilterChain(
new AntPathRequestMatcher("/saml/logout/**"),
samlLogoutFilter()));
chains.add(new DefaultSecurityFilterChain(
new AntPathRequestMatcher("/saml/metadata/**"),
metadataDisplayFilter()));
chains.add(new DefaultSecurityFilterChain(
new AntPathRequestMatcher("/saml/SSO/**"),
samlWebSSOProcessingFilter()));
chains.add(new DefaultSecurityFilterChain(
new AntPathRequestMatcher("/saml/SingleLogout/**"),
samlLogoutProcessingFilter()));
return new FilterChainProxy(chains);
}

Key manager

Všechny SAML zprávy, jež si IdP a SP vyměňují jsou podepsané privátním klíčem dané strany. Doporučuji mít pro SAML podpisový klíč separátní key store (nemíchat ho třeba s key storem, který potřebuje aplikační server pro HTTPS).

V naší ukázkové aplikaci je SAML key store na classpath - v jakémkoli jiném, než lokálním vývojovém prostředí, key store samozřejmě externalizujeme (nepřibalujeme do WARu) a hesla kryptujeme.

@Bean
public KeyManager keyManager() {
DefaultResourceLoader loader = new DefaultResourceLoader();
Resource storeFile = loader.getResource("classpath:/saml/samlKeystore.jks");
String storePass = "secure";
Map<String, String> passwords = new HashMap<>();
passwords.put("samuraj", "secure");
String defaultKey = "samuraj";
return new JKSKeyManager(storeFile, storePass, passwords, defaultKey);
}

Podepisování SHA-256

V minulém díle jsem zmiňoval, že Spring SAML defaultně používá při podepisování algoritmus SHA-1, kdežto ADFS očekává SHA-256. Jedna strana se musí přizpůsobit. Doporučuji upravit aplikaci - použít SHA-256 není nic těžkého.

Výběr podpisového algoritmu se provádí při inicializaci SAMLu pomocí třídy SAMLBootstrap, která bohužel není konfigurovatelná. Pomůžeme si tak, že od třídy podědíme a potřebný algoritmus podstrčíme:

public class SamlBootstrapSha256 extends SAMLBootstrap {
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory)
throws BeansException {
super.postProcessBeanFactory(beanFactory);
BasicSecurityConfiguration config =
(BasicSecurityConfiguration) Configuration.getGlobalSecurityConfiguration();
config.registerSignatureAlgorithmURI(
"RSA", "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256");
}
}
V konfiguraci pak třídu instancujeme následujícím způsobem. Mimochodem, povšimněte si, že beana je instancovaná jako static. To proto, aby inicializace proběhal velmi záhy při vytváření kontextu.

@Bean
public static SAMLBootstrap samlBootstrap() {
// return new SAMLBootstrap();
return new SamlBootstrapSha256();
}

That's All Folks!

Tím se náš 3-dílný mini seriál o Spring Security, SAMLu a ADFS uzavírá. Samozřejmě, že bych mohl napsat ještě mnoho odstavců a nasdílet spoustu dalších gistů. Ale už by to bylo jen nošení housek do krámu.

Lepší bude, pokud si teď stáhnete ukázkový projekt sw-samuraj/blog-spring-security, trochu se povrtáte ve zdrojácích a na závěr v něm vyměníte soubor FederationMetadata.xml a zkusíte ho rozchodit vůči vašemu ADFS. Při troše štěstí by to mělo fungovat na první dobrou :-)

Jako bonus pro odvážné - pokud se opravdu pustíte do těch zdrojových kódů - můžete v historii projektu najít další Spring Security ukázky (je to celkem rozumně otagovaný):
  • Výměna CSRF tokenu mezi Springem a Wicketem (tag local-ldap).
  • Multiple HttpSecurity - v jedné aplikaci: autentikace uživatele přes formulář a mutual-autentication REST služeb přes certifikát (tag form-login).
  • Autentikace vůči lokálnímu (embedovanému) LDAPu (tag local-ldap).
  • Autentikace vůči Active Directory (tag remote-ad).

Související články


3 komentáře:

  1. Ahoj,

    myslim, ze SHA256 by melo byt by default v spring-security-saml

    Udelal jsem pull request, uvidime.

    https://github.com/spring-projects/spring-security-saml/pull/239

    OdpovědětVymazat
    Odpovědi
    1. Výborně! Doufám, že se to tam dostane. Už jsem s nimi taky něco řešil (vlastně verzi 1.0.3 vydali v podstatě pro mě :-) a bylo to celkem rychlé.

      Vymazat
  2. Ahoj,

    jak jsi resil, ze IDP metadata ziskane z ADFS jsou podepsane klicem.
    Pridavam ten klic manualne do keystoru, jinak dostanu chybu, ze neni mozny verifikovat podepsana IDP metadata z ADFS.

    Prijde mi zvlastni, ze ten klic je pribaleny do metadat...

    Zacinam mit vic konfiguraci pro ruzne skupiny uzivatelu a chtel bych nejak automatizovat manipulaci s klicema. Moc se mi nechce brat ten verejny klic na verifikaci podpisu metadat z toho vlastniho requestu.

    Muj plan je udelat gui pro manipulaci s klicema pro kazdou skupinu uzivatelu zvlast. Narazim na to, ze nevim jak ty klice pak pouzivat v spring security saml. Tam je k dispozici jen alias a nejsem schopnej si vybrat klic...
    Jak jsi resil manipulaci s klicema?

    Dalsi vec, ktera mi vadi je, ze po pridani noveho klice musim restartovat aplikaci, aby se klic znova nacetl.

    OdpovědětVymazat

Poznámka: Komentáře mohou přidávat pouze členové tohoto blogu.