In der heutigen heterogenen Systemlandschaft finden sich viele verschiedene Systeme die über Schnittstellen (API) miteinander kommunizieren müssen. Dank des HTTP-Protokolls ist dies problemlos möglich. Um die API Schnittstelle zu schützen und nur autorisierten Clients den Zugriff zu gewähren, hat sich das OAuth-Protokoll als de facto Standard etabliert.

Im Folgenden will ich zeigen, wie man mit Spring einen OAuth-Server und einen OAuth-Client, mit der OAuth Version 2 entwickelt. Version 1 besitzt eine Sicherheitslücke, weshalb seit 2012 diese neue Version genutzt wird.

Was ist das OAuth-Protokoll

Das OAuth-Protokoll erlaubt einen autorisierten Zugriff auf Webressourcen einer Anwendung A (z.B. Google-Kalender) aus einer anderen Anwendung B (z.B. Reise-Portal) ohne, das Anwendung B die Zugangsdaten für die Anwendung A benötigt. Die folgende Abbildung zeigt ein Beispiel:

Beispiel zu Verwendung von OAuth

Abbildung 1: Beispiel zu Verwendung von OAuth

Da sich bereits viele Leute die Mühe gemacht haben, das Protokoll verständlich zu erläutern möchte ich an diese Stelle darauf verweisen:

In meinem Beispiel soll der OAuth-Client (genannt Client) eine geschützte Ressource (Login Name) des Nutzers per API auf dem OAuth-Server (genannt Sever) abfragen. Der Client schickt hierfür den Nutzer per Redirect zum Server, um sich die Anfrage autorisieren zu lassen. Wenn der Nutzer seine Zustimmung erteilt, erhält der Client einen Access-Token. Mit diesem Token authentifiziert sich der Client während der API Anfragen und kann so die geschützte Ressource abrufen. Da dieser Token gleichwertig mit dem Nutzer-Credentials ist, sollte die HTTP-Kommunikation in produktiven Umgebungen immer über SSL geschützt sein.

OAuth Prozess

Die folgende Abbildung soll den prinzipiellen Ablauf des Prozesses darstellen. Dabei wird der Prozess stark vereinfacht gezeigt, um die Grundidee zu verdeutlichen. Tatsächlich sind als Zwischenschritte noch weitere Redirects und Anfragen enthalten.

Prozessablauf OAuth

Abbildung 2: Prozessablauf OAuth

Da bereits einige OAuth Beispiele für Spring mit einer annotationsbasierten Konfiguration existieren, möchte ich an dieser Stelle zeigen, wie eine Variante mit klassischer XML-Konfiguration aussieht.

Als Ausgangspunkt wurde ein Maven-Spring-Demo Projekt mittels Spring Tool Suite erzeugt und entsprechend erweitert. Das Maven-Projekt beinhaltet zwei Module: den OAuth-Server und den OAuth-Client. Da der Client eine Web-Anwendung (serverbasiert) ist und somit im OAuth-Kontext als vertrauenswürdiger Client-Typ eingeordnet ist, wird als Authorization Grant „Authorization Code“ genutzt. Das bedeutet, dass sich der Client – zusätzlich zum Access-Token (repräsentiert die Nutzer-Credentials) – mit eigenen Login-Daten gegenüber dem des Server authentifiziert.

Marketing & Sales Lösungen - Ihr Wettbewerbsvorteil
Erfolgreiche und effiziente Aktivitäten in den Bereichen Marketing & Sales benötigen vor allem Planung, Monitoring und passende Supportlösungen.

Der OAuth-Server

Im Folgenden sind Ausschnitte der root-context.xml des OAuth-Server gezeigt:

<!-- the authorization-server with the supported authentication grants -->
<oauth:authorization-server client-details-service-ref="clientDetailsService" token-services-ref="tokenService">
    <oauth:authorization-code/>
    <!-- other Authorization Grant explicit disabled-->
    <oauth:implicit disabled="true"/>
    <oauth:refresh-token disabled="true"/>
    <oauth:client-credentials disabled="true"/>
    <oauth:password disabled="true"/>
</oauth:authorization-server>

<!-- stores the access token to remember the oauth clients. -->
<bean id="tokenService" class="org.springframework.security.oauth2.provider.token.DefaultTokenServices">
    <property name="tokenStore" ref="tokenStore"/>
    <property name="clientDetailsService" ref="clientDetailsService"/>
    <property name="accessTokenValiditySeconds" value="43200"/>
</bean>

<!-- the token store as in-memory-store -->
<bean id="tokenStore" class="org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore"/>

Als erstes wird der authorization-server definiert. Über den referenzierte tokenService weiß dieser welche Access-Token existieren und ob diese noch gültig sind. Zum Speichern der Access-Token nutzt der tokenService einen InMemoryTokenStore, welcher über das Attribut tokenStore referenziert wird. Optional könnte man hier, mit einem JdbcTokenStore, die Token auch in einer Datenbank speichern. Das wird spätestens dann relevant, wenn man mit Refresh-Token arbeiten will. Ein Refresh-Token erlaubt es, eine erneute Autorisierung durch den Nutzer (Abb. 2: Schritt 2) zu vermeiden, wenn der Access-Token abgelaufen ist.

<!--  the clientDetailsService knows a stores of the oauth clients-->
<bean id="clientDetailsService" class="org.springframework.security.oauth2.provider.client.InMemoryClientDetailsService" autowire="constructor">
    <!-- the client store as in-memory-map -->
    <property name="clientDetailsStore">
        <map>
            <entry key="${clientId}">
                <bean class="org.springframework.security.oauth2.provider.client.BaseClientDetails">
                    <!-- the placeholder ${clientId} & ${clientSecret} will be populated due to spring  -->
                    <property name="clientId" value="${clientId}"/>
                    <property name="clientSecret" value="${clientSecret}"/>
                    <property name="scope" value="loginNameLesen"/>
                    <property name="authorizedGrantTypes">
                        <list>
                            <value>authorization_code</value>
                            <value>refresh_token</value>
                        </list>
                    </property>
                </bean>
            </entry>
        </map>
    </property>
</bean>

<!-- allow the use of property placeholter like ${clientId} -->
<context:property-placeholder location="classpath:/META-INF/spring/application.properties"/>

Der clientDetailsService kennt über den clientDetailsStore die zulässigen Clients und deren Login-Daten. Zur Vereinfachung werden die Client-Credentials als Map im clientDetailsStore hinterlegt. Alternativ könnte hier der JdbcClientDetailsService genutzt werden, um die Clients aus einer Datenbank zu laden. Die Platzhalter für ClientId und ClientSecret werden aus der application.properties durch Spring befüllt. Außerdem wird als benötiger Scope „loginNameLesen“ definiert. Dieser Scope wird dem Nutzer während der Autorisierungs-Anfrage (Abb. 2: Schritt 2) angezeigt und erlaubt somit eine „Art“ Rechtevergabe an Clients.

Die zwei benötigen Endpoints zur Realisierung des OAuth Protokolls definiert der authorization-server.

Als erstes den Authorize-Endpoint, welcher für die Behandlung der Autorisierungsanfrage (Abb. 2: Schritt 2) zuständig ist und auf die URL „/oauth/authorize“ lauscht. Das OAuth-Protokoll schreibt keine URLs vor, aber die meisten Implementierungen nutzten die genannte URL als Standard-Einstellung. In diesem Beispiel soll uns die Spring Standard-Seite genügen und wir sparen uns dadurch die Definitionen für diesen Enpoint.

<!-- the endpoint to change the authorize_code to an access-token -->
<security:http pattern="/oauth/token" create-session="stateless"
    authentication-manager-ref="clientAuthenticationManager" auto-config="false">
    <security:intercept-url pattern="/oauth/token" access="IS_AUTHENTICATED_FULLY" />
    <security:anonymous enabled="false"/>
    <security:http-basic entry-point-ref="clientAuthenticationEntryPoint"/>
    <security:access-denied-handler ref="oauthAccessDeniedHandler"/>
</security:http>

<!-- send a http 401 with the error text if the login failed  -->
<bean id="clientAuthenticationEntryPoint" class="org.springframework.security.oauth2.provider.error.OAuth2AuthenticationEntryPoint"/>

<!-- send a http 403 with the error text if the login failed  -->
<bean id="oauthAccessDeniedHandler" class="org.springframework.security.oauth2.provider.error.OAuth2AccessDeniedHandler"/>

<!-- the clientAuthenticationManager knows the oauth clients -->
<security:authentication-manager id="clientAuthenticationManager">
    <security:authentication-provider user-service-ref="clientDetailsUserService"/>
</security:authentication-manager>

<!-- gives access to the client detail information like id & secret  -->
<bean id="clientDetailsUserService" class="org.springframework.security.oauth2.provider.client.ClientDetailsUserDetailsService">
    <constructor-arg ref="clientDetailsService"/>
</bean>

Der zweite benötigte Endpoint ist der Token-Endpoint, um den Access-Token an den Client herauszugeben. Als Standard-URL hat sich „/oauth/token“ etabliert. Da der Token-Endpoint von den Clients angefragt wird, muss er deren Login-Credentials kennen und wird daher mit dem clientAuthenticationManager verbunden. Dieser benötigt einen clientDetailsUserService den wir mit unserem bereits vorhanden clientDetailsService verknüpfen. Damit der Token-Endpoint nur auf berechtigte Anfragen antwortet, wird per Interceptor-access „IS_AUTHENTICATED_FULLY“ gefordert. Da sich die Client Anfragen per Access-Token und Client-Credentials authentifizieren, muss keine Session vorhanden sein. Also kann der Endpoint mit create-session=“stateless“ definiert werden.

<!-- the api endpoint to allow oauth-clients to receive secured data -->
<security:http use-expressions="true" pattern="/apiUrl" auto-config="false">
    <!-- allow oauth clients & normal user -->
    <security:intercept-url pattern="/apiUrl" access="#oauth2.hasScope('loginNameLesen')"/>
    <security:anonymous enabled="false"/>
    <security:http-basic entry-point-ref="clientAuthenticationEntryPoint"/>
    <security:custom-filter ref="resourceServer" before="PRE_AUTH_FILTER"/>
    <security:expression-handler ref="oauthWebExpressionHandler"/>
    <security:access-denied-handler ref="oauthAccessDeniedHandler"/>
</security:http>

<!--  the web expression handler allows security-access expressions like "#oauth2.isUser()" -->
<oauth:web-expression-handler id="oauthWebExpressionHandler"/>

<!--  the resource-server can be uses as a filter in the <http/> element to give access to client
    information (e.g. scope) by its access token -->
<oauth:resource-server id="resourceServer" token-services-ref="tokenService"/>

Jetzt haben wir es fast geschafft. Wir brauchen noch unseren eigentlichen API-Endpoint, der den Login-Namen des Nutzers an autorisierte Clients zurückliefert. Als URL nutzte ich dafür „/apiUrl“. Über die Security-Expression „#oauth2.hasScope(‚loginNameLesen‘)“ wird gefordert, dass es sich um einen OAuth-Client-Request handelt mit dem Scope „loginNameLesen“. Damit „#oauth2“ als Expression nutzbar ist, benötigen wir den oauthWebExpressionHandler. Der „resourceServer“, agiert schließlich als custom-filter innerhalb unseres Endpoints. Er befüllt die Authentication-Instanz im Security-Context, abhängig vom Access-Token, mit den Client-Informationen (zB. dem Scope). Diese Informationen erlauben es dem oauthWebExpressionHandler, die entsprechende Security-Expression auszuwerten. Der „resourceServer“ benötigt für seine Aufgabe eine Referenz zum tokenService.

<!-- the normal security config for the user login -->    
<security:http auto-config='true' use-expressions="true">
    <security:intercept-url pattern="/**" access="hasRole('ROLE_USER')"/>
</security:http>

<!-- the authenticationManager that knows the user logins -->
<security:authentication-manager>
    <security:authentication-provider>
        <security:user-service>
            <security:user name="server" password="secret" authorities="ROLE_USER"/>
        </security:user-service>
    </security:authentication-provider>
</security:authentication-manager>

Der Rest der Context-Definition beschreibt eine minimale Security-Konfiguration, um sich als User mit dem Login „server“ und dem Passwort „secret“ anzumelden.

Die Implementierung der Controller-Logik für den Server ist vergleichsweise einfach. Im Folgenden sieht man den einzigen Controller des OAuth-Servers Modules, in der Datei ServerController.java.

@Controller
public class ServerController {

    @RequestMapping(value = "/apiUrl", method = RequestMethod.GET)
    @ResponseBody
    public String apiCall(final Authentication authentication) {
        System.out.println("send data to oAuth-Client...");
        return "hello from oAuth-server. Your login name is "+authentication.getName();
    }
    
    @RequestMapping(value = "/", method = RequestMethod.GET)
    public String home(final Authentication authentication, final ModelMap model) {
        System.out.println("Welcome home @ server!");
        model.addAttribute("loginName",authentication.getName());
        return "home";
    }
}

Über einen Spring Componenten-Scan wird der annotierte Controller registriert. Er bearbeitet sowohl Anfragen auf die Home-Seite als auch Anfragen für den geschützten API-Endpoint.

Der OAuth-Client

Die Implementierung der clientseitigen Controller-Logik ist ähnlich einfach, wie die des Servers. Der folgende Code zeigt den einzigen Controller der Client-Anwendung, in der Datei ClientController.java.

@Controller
public class ClientController {

    @Qualifier("oauth-rest-template")
    @Autowired
    private RestTemplate restTemplate;
    
    @Value("${oAuthServer.api.url}")
    private String apiUrl;
     
    @RequestMapping(value="/apiCall", method = RequestMethod.GET)
    public String apiCall(final Model model) {
        System.out.println("call the oAuth-server api: "+this.apiUrl);
        ResponseEntity<String> result = this.restTemplate.getForEntity(this.apiUrl, String.class);
        System.out.println("api result: Code "+result.getStatusCode()+" - "+result.getBody());
        
        model.addAttribute("oAuthServerData", result.getBody());

        return "ApiCallResult";
    }
    
    @RequestMapping(value= "/", method = RequestMethod.GET)
    public String home() {
        System.out.println("Welcome home @ client!");
        return "home";
    }
}

Mittels Spring Componenten-Scan wird auch dieser Controller registriert. Er bearbeitet sowohl Anfragen auf die Home-Seite als auch Anfragen für eine Seite, die die geschützte Server-Ressource anzeigt. Dafür wird innerhalb der Methode „apiCall“ ein durch Spring injiziertes RestTemplate genutzt, um die ebenfalls injizierte Server Ressource-URL abzufragen.

Jetzt fehlt uns nur noch die Definition der root-context.xml für den Client, die das benötigte RestTemplate „oauth-rest-template“ erstellt:

<!-- the rest template to communicate with the oauth server -->
<oauth:rest-template id="oauth-rest-template" resource="oauth-resource" access-token-provider="accessTokenProviderChain"/>

<!-- the oAuth server with the protected resource. The server & this client share the client_id & secret & scope -->
<oauth:resource id="oauth-resource" type="authorization_code" client-id="${clientId}"
    client-secret="${clientSecret}"
    access-token-uri="${oAuthServer.url}/oauth/token" user-authorization-uri="${oAuthServer.url}/oauth/authorize"
    scope="${oAuthServer.api.scope}" />

Wie zu erkennen ist, benötigt das RestTemplate eine Definition der OAuth-Ressource, die durch den Server bereitgestellt wird. Diese OAuth-Ressource beschreibt also den API-Endpoint des Servers auf Client-Seite. Sie benötigt die Client-Credentials sowie den anzufragenden Scope. Außerdem müssen die URLs zum Authorize-Endpoint und Token-Endpoint definiert werden. Die Platzhalter „${oAuthServer.url}“ und „${clientId}“ werden wieder aus der application.properties Datei durch Spring befüllt.

Außerdem wird eine Referenz auf die „accessTokenProviderChain“ innerhalb des RestTemplates definiert. Für den Fall, dass das Access-Token unbekannt oder ungültig ist, wir die „accessTokenProviderChain“ befragt. Diese ProviderChain nutzt wiederum die Klasse AuthorizationCodeAccessTokenProvider um einen Access-Token mittels Authorization Grant „Authorization Code“ zu erhalten. Wenn ein Refresh-Token zum Einsatz kommen soll, kann innerhalb der ProviderChain ein „clientTokenServices“ definiert werden, der z.B. als „JdbcClientTokenServices“ das Refresh-Token zum Nutzer aus einer Datenbank liest.

Damit nun die ganze Magie des OAuth-Protokolls mit seinen Redirects usw. funktioniert, benötigen wir noch einen „oauth2ClientContextFilter“. Dieser Filter wird in die Spring-Filter-Chain eingebunden und behandelt die UserRedirectRequiredException. Diese Exception wird durch den AuthorizationCodeAccessTokenProvider geworfen während der Ermittlung eines neuen Access-Token. Der „oauth2ClientContextFilter“ nutzt die Informationen der Exception, um den Browser des Nutzers zum OAuth-Server umzuleiten und das Access-Token abzufragen (Abbildung 2: Schritt 2).

Der Rest der Context-Definition beschreibt wieder eine minimale Security-Konfiguration, um sich als User mit dem Login „client“ und dem Passwort „secret“ anzumelden.

Wir finden individuelle Lösungen
Planungs- und Entscheidungsprozesse in Unternehmen basieren auf zahlreichen Daten und deren Gewichtung. Wir finden individuelle Lösungen, die Ihren speziellen Anforderungen entsprechen und binden bestehende Systeme optimal ein.

Mögliche Erweiterungen & Anpassungen

Die erste – und sicherlich sinnvollste – Erweiterung ist der Austausch der InMemory-Tokenstores,
durch eine persistente Variante, um die Vorteile eines Refresh-Tokens nutzen zu können. Auch das Erstellen einer eigenen Autorisierungs-Seite für den Server, um dem Nutzer den angefragten Scope zu zeigen, ist sicherlich sinnvoll.

Auch könnte man auf den Login mittels Nutzer/Passwort innerhalb des Client verzichten, in dem man den Server als Login-Quelle nutzt. Hierfür kann die Klasse „OAuth2ClientAuthenticationProcessingFilter“ genutzt werden, um mittels RestTemplate ein Access-Token zum Nutzer abzurufen und diese als Authentication-Objekt innerhalb des Clients zu verwenden.

Wenn man sogar noch einen Schritt weiter gehen will, kann mittels OpenID-Connect ein SingleSignOn-System aufgebaut werden. OpenID-Connect basiert dabei auf dem OAuth Protokoll und erweitert es genau für den Zweck des SingleSignOn.

Fazit

Dank Spring ist es verhältnismäßig einfach einen eigenen OAuth-Server und Client aufzusetzen. Man sollte jedoch ein gutes Verständnis des Protokolls haben, um mögliche Fehlermeldungen deuten zu können, welche bei der Implementierung entstehen. Auch ist es wichtig zu wissen, dass einige große Anbieter das OAuth-Protokoll nicht immer Spezifikationskonform implementiert haben, wodurch es beim Anbinden zu Überraschungen kommen kann.

Bei der Fehlersuche hilft es, das Logging-Framework für das Package „org.springframework.security.oauth2“ auf DEBUG zu stellen. Denn so mancher Vertippter bei den Client-Credentials wird nicht unbedingt mit einer aussagekräftigen Fehlermeldung abgewiesen. Das Log zeigt dann den wahren Grund des Problems.

Diskutieren Sie mit!

Es steht Ihnen frei einen Kommentar zu hinterlassen. Sie finden Informationen zur Verwendung Ihrer Daten in unserer Datenschutzerklärung.

Alle mit einem markierten Felder sind Pflichtfelder und müssen ausgefüllt werden