In diesem Artikel möchte ich dir zeigen, wie du mit Hilfe, des Azure ADs deine Spring Boot- und Angular-Applikation absichern kannst. Dieser Beitrag richtet sich an Personen, die bereits Grundkenntnisse in Angular und Spring mitbringen. Einige Begrifflichkeiten in Verbindung mit OAuth werden nicht erläutert, da dies den Umfang des Beitrages sprengen würde. Den Sourcecode findest du in diesem GIT-Repo.
Azure AD
Als Erstes wollen wir uns das Azure AD genauer anschauen, da dieses hauptsächlich für die Verwaltung von Nutzern sowie Zugriffsberechtigungen verantwortlich ist.
Für unser Beispiel brauchen wir zwei Konfigurationen:
- Einen Public-Client
- Einen Service-Client
Der Public-Client dient dazu, dass sich unsere Nutzer an der Oberfläche anmelden und/oder registrieren können. Zusätzlich wird das Backend mit diesem Client abgesichert, so dass nur angemeldete Nutzer das Backend ansprechen können. Der Service-Client dient dazu, um z.B. externe Ressourcen wie den Microsoft Graph anzusprechen. Darauf werden wir später noch näher eingehen.
Konfiguration
Wie konfigurieren wir nun den Azure AD? Dazu gehen wir in das Azure Portal und suchen über die Suchleiste nach „Azure Active Directory“, sofern dies nicht auf der Startseite angezeigt wird. In der darauffolgenden Oberfläche können wir nun unter dem Reiter „App Registration“ eine neue Konfiguration hinzufügen.
Public-Client
Nun können wir einen Namen vergeben. Dieser sollte app-spezifisch sein und wird in unserem Beispiel „my-app“ heißen. Im nächsten Schritt können wir spezifizieren welche Art von Nutzern sich anmelden dürfen. Hier behalten wir die Standarteinstellung „Account in this organizational Directory only“ bei, dadurch können sich nur Nutzeranmelden, die zu unserem Unternehmen gehören. Zuletzt können wir nun die „Redirect URI“ angeben, dabei werden wir die lokale Angular CLI referenzieren (http://localhost:4200). Bei der Plattform wählen wir dann „Single Page Application (SPA)“ aus. Diese Konfiguration dient dazu um den Nutzer, nach erfolgreicher Anmeldung, zurück auf unsere Webapp zu leiten. Anschließend auf den Button „Register“ klicken, um den Vorgang abzuschließen.
Nun befinden wir uns in der Übersicht unseres Clients, hier finden wir wichtige Informationen, die wir später brauchen, um unsere SPA anzubinden. Darauf werden wir näher eingehen, wenn wir unseren Client implementieren.
Service Client
Um einen „Service Client“ einzurichten, gehen wir genauso vor wie bei dem Public Client:
- Im Azure AD gehen wir auf App Registration
- Tragen einen Namen ein z.B. „my-app-service-client“
- Wählen die entsprechenden Ristrektionen aus
- Schließen die Registration ab
Wie dir aufgefallen sein mag, haben wir in diesem Schritt keine „Redirect URI“ angeben. Dies hat den Hintergrund, dass wir diese bei dem Service Client nicht brauchen.
Warum brauchen wir die „Redirect URI“ nicht? Das liegt daran, dass der „Service Client“ kein richtiger Nutzer ist. Wir melden uns hierbei nicht mit nutzerspezifischen Daten wie Benutzername und Passwort an. Sondern haben eine Client-ID und ein Client-Secret, mit denen wir uns einen Access-Token vom Azure AD anfordern. Mit diesem Token können wir dann andere Systeme ansprechen, wie z.B. den Microsoft Graph. Dieser hält nutzerspezifische Informationen bereit. Später im Beispiel werden wir diesen nutzen, um uns das Profilbild zum angemeldeten Nutzer zu holen.
Wichtiger Hinweis:
Vorab noch ein wichtiger Hinweis: sollte der Access-Token in dem späteren Beispiel nicht akzeptiert werden, bietet es sich an zuerst die Token-Version zu überprüfen. Dies kann man mit Hilfe von jwt.io herausfinden. Sollte der Access-Token die Version 1.0 haben, muss das Manifest der App-Registration angepasst werden. Hier muss folgender Parameter in das Manifest eingetragen/angepasst werden: „”accessTokenAcceptedVersion”: 2,“. Anschließend speichern, danach sollte alles funktionieren.
Projekt Setup
Da wir jetzt die initiale Konfiguration für unseren Azure AD vorgenommen haben, können wir mit der Implementierung unserer Demo-Anwendung anfangen.
Springboot
Für unsere Demo solltest du dir ein SpringBoot Projekt erstellen und folgende Dependencies hinzufügen:
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-oauth2-client
org.springframework.boot
spring-boot-starter-oauth2-resource-server
org.springframework.boot
spring-boot-starter-security
com.azure.spring
spring-cloud-azure-starter
org.springframework.boot
spring-boot-starter-webflux
Als Erstes erstellen wir uns einen einfachen „HelloWorldController“. Dieser beinhaltet einen REST-Endpunkt und sieht wie folgt aus:
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloWorldController {
@GetMapping
public String helloWorld() {
return "Hello World!";
}
}
Um sicher zu gehen, dass bis jetzt alles funktioniert, können wir unsere SpringBoot-App starten und den Endpunkt ansprechen. Hierzu kann man Postman oder cURL verwenden.
Kommt ein „Hello World!“ als Response zurück, hat alles funktioniert.
Resource Server Konfiguration
Damit wir unseren Endpunkt absichern können, müssen wir eine neue Security-Konfiguration erstellen. Dazu erstellen wir eine neue Klasse mit dem Namen „ResourceServerSecurityConfig“:
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceServerSecruityConfig {
@Bean
public DefaultSecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.authorizeRequests(auth -> auth.anyRequest().authenticated())
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);
return httpSecurity.build();
}
}
Mit der Annotation „@EnableWebSecurity“ werden alle Konfigurationen, die für unseren „ResourceServer“ notwendig sind, automatisch hinzugefügt. Um das Standardverhalten zu ändern, überschreiben wir die Klasse „DefaultSecurityFilterChain“. Somit haben wir die Möglichkeit zu spezifizieren, welche Pfade wir absichern möchten. Natürlich gibt es noch viel mehr, was sich hier konfigurieren lässt. Für mehr Informationen kannst du gerne in die Spring-Dokumentation schauen, da ich in diesem Artikel nicht näher darauf eingehen werde.
Damit alles funktioniert, fehlt uns aber noch eine Sache: Die Konfiguration der URL zu unserem Azure AD. Diese findest du auf der Übersichtsseite deiner App-Registrierung. Dort befindet sich ein Button „Endpoints“. Wenn du auf diesen Klicks öffnet sich eine Sidebar mit den wichtigsten URLs. Hier kopieren wir uns die „OpenID Connect metadata document“ URL (https://login.microsoftonline.com/{DEINE_TENANT_ID}/v2.0/.well-known/openid-configuration). Anschließend führen wir in Postman ein GET-Request auf diese URL aus, in der Response befinden sich dann alle verfügbaren Endpunkte, die der Azure AD zur Verfügung stellt, sowie einige Konfigurationsparameter. Hier kopieren wir uns die „jwks_uri“ und fügen sie in die „applications.properties/.yaml“ hinzu.
spring.security.oauth2.resourceserver.jwt.jwk-set-uri=${RESOURCE_SERVER_URL}
Wenn wir nun unsere Applikation starten und einen GET-Request auf unseren Endpunkt ausführen, sollten wir eine leere Response mit dem Statuscode 401 zurückbekommen.
Anbinden des Frontends
Um später die Endpunkte in unserem Frontend nutzen zu können, müssen wir für eine Möglichkeit sorgen, dass der Nutzer sich anmelden kann. Hierfür brauchen wir zuerst eine passende OAuth-Bibliothek. Dazu werden wir die „angular-outh2-oidc“ Bibliothek verwenden. Diese ist OpenID zertifiziert und kann für alle Security Token Services (STS), die sich an den OAuth-Standard halten, verwendet werden. Hinzufügen kannst du sie mit
npm install angular-oauth2-oidc
Konfiguration und der App-Inizializer
Als Erstes müssen wir die Bibliothek in unserer „AppModule“ hinzufügen und konfigurieren.
Hierfür legen wir in der „app.module.ts“ zunächst eine neue Konstante vom Typ „AuthConfig“ an und füllen sie mit den Werten unseres Azure ADs:
const authConfig: AuthConfig = {
issuer: "https://login.microsoftonline.com/{TENANT_ID}/v2.0",
redirectUri: window.location.origin,
clientId: '{CLIENT_ID}’,
responseType: 'code',
strictDiscoveryDocumentValidation: false,
scope: 'openid profile offline_access api://{APPLICATION_ID}/app'
}
Die Applikations-ID findest du in deiner App-Registration unter dem Bereich „Expose an Api“.
Nun bereiten wir den App-Initializer vor. Dieser führt dazu, dass der Nutzer sich einloggen muss, bevor er die Webseite betreten kann. Natürlich kann man den Login auch über einen Button realisieren.
Der App-Initializer sieht wie folgt aus und wird später der Konfiguration hinzugefügt:
export function initializerFactory(oauthService: OAuthService): () => Promise {
return () => {
oauthService.configure(authConfig);
return oauthService.loadDiscoveryDocumentAndLogin();
}
}
Die Funktion nimmt eine Instanz vom Typ „OAuthService“ entgegen. Dieser wird von der Bibliothek mitgeliefert. Als nächstes konfigurieren wir den Service mit dem Objekt, welches wir vorher mit den entsprechenden Parametern gefüllt haben. Zum Schluss wird dann der Login-Flow initialisiert und der Nutzer aufgefordert, sich mit seinem Microsoft-Account einzuloggen.
Die Konfiguration des „AppModules“ sieht dann wie folgt aus:
declarations: [
AppComponent
],
imports: [
BrowserModule,
HttpClientModule,
OAuthModule.forRoot({
resourceServer: {
sendAccessToken: true
}
})
],
providers: [{
provide: APP_INITIALIZER,
useFactory: initializerFactory,
deps: [OAuthService],
multi: true
}],
In den Imports fügen wir das „OAuthModule“ hinzu und konfigurieren dies so, dass beim jedem Rest-Request der Access-Token, der nach der erfolgreichen Anmeldung mitgeliefert wird, mit gesendet wird. Dadurch brauchen wir keinen eigenen Interceptor implementieren, der dies für uns übernehmen würde.
Wenn wir nun das Angular Projekt starten und auf localhost:4200 navigieren, sollten wir zur Anmeldeseite von Microsoft geleitet werden. Nachdem wir uns angemeldet haben, werden wir zurück auf unsere Webapp navigiert.
Laden des Profilbilds
Unsere Webapp sieht aktuell noch leer aus, also wird es Zeit diese mit ein wenig Inhalt zu füllen. Als Erstes werden wir uns das Profilbild des aktuell eingeloggten Nutzers vom Microsoft Graph anfordern.
Webclient
Zunächst brauchen wir einen Webclient, um die Anfrage aus unserem SpringBoot heraus an den Ms Graph zu senden. Dazu werden wir in diesem Beispiel Webflux verwenden.
Webclient Konfiguration
Zunächst müssen wir eine neue Konfiguration für unseren Client implementieren, so dass wir uns gegen den MS Graph authentifizieren, können bzw. die Authentifizierung bei der Nutzung des Clients automatisch vorgenommen wird.
MsGraphWebclientConfig:
@Bean
ReactiveClientRegistrationRepository getRegistration(
@Value("${spring.security.oauth2.client.provider.azuread.token-uri}") String tokenUri,
@Value("${spring.security.oauth2.client.registration.azuread.client-id}") String clientId,
@Value("${spring.security.oauth2.client.registration.azuread.client-secret}") String clientSecret,
@Value("${spring.security.oauth2.client.registration.azuread.scope}") String scope
) {
ClientRegistration registration = ClientRegistration
.withRegistrationId("azuread")
.tokenUri(tokenUri)
.clientId(clientId)
.clientSecret(clientSecret)
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
.scope(scope)
.build();
return new InMemoryReactiveClientRegistrationRepository(registration);
}
@Bean(name = "azuread")
WebClient webClient(ReactiveClientRegistrationRepository clientRegistrations) {
InMemoryReactiveOAuth2AuthorizedClientService clientService = new InMemoryReactiveOAuth2AuthorizedClientService(clientRegistrations);
AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager authorizedClientManager = new AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(clientRegistrations, clientService);
ServerOAuth2AuthorizedClientExchangeFilterFunction oauth = new ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
oauth.setDefaultClientRegistrationId("azuread");
return WebClient.builder()
.filter(oauth)
.build();
}
Mit der Methode “getRegistration” fügen wir eine neue Client-Konfiguration dem „ReactiveClientRegistrationRepository“ hinzu. Dieses Repository kann eine oder mehrere Client-Konfigurationen halten. Mittelst der „RegistrationId“ können wir die Konfiguration auslesen und für den WebClient verwenden. Die Parameter, die wir für die Konfiguration benötigen, lesen wir mit Hilfe der Annotation „@Value“ und der entsprechenden Referenz in unserer Properties-Datei aus. Wie vorher schon beschrieben, melden wir uns hier nicht mit einem Nutzer an, sondern holen uns einen Access-Token durch die Verwendung der Client-ID und des Secrets. Damit dies funktioniert und der Azure AD weiß, dass wir uns mit diesen Daten anmelden wollen. Setzen wir den „authorizationGrantType“ auf „CLIENT_CREDENTIALS“. Zum Schluss wird nun der Webclient konfiguriert, hierzu wird die vorher definierte „ReactiveClientRegistration“ verwendet.
Der Service
MsGraphService:
@Service
@Slf4j
public class MsGraphService {
@Autowired
@Qualifier("azuread")
private WebClient webClient;
@Value("${ms.graph.users.baserURI}")
private String msGraphBasURI;
public byte[] getProfileImageForLoggedInUser(final String size) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String emailClaim = ((Jwt) authentication.getPrincipal()).getClaimAsString("preferred_username");
Mono retVal = webClient.get()
.uri(msGraphBasURI
+ emailClaim + "/photos/"
+ size + "/$value")
.retrieve().bodyToMono(byte[].class);
return retVal.block();
}
}
In diesem Service holen wir uns mit Hilfe der vorher definierten Client-Konfiguration eine Webclient Instanz. Mit der Methode „getProfileImageForLoggedInUser“ holen wir uns aus dem „SecruityContext“ den Access-Token, der aus dem Frontend mitgeschickt wird, und lesen die E-Mail-Adresse aus. Für meinem Fall steht diese im Claim „preferred_username“. Dies kannst du prüfen, in dem du dir den Access-Token ausliest, z.B. über den Netzwerkreiter im Browser. Anschließend kannst du den Token über jwt.io decodieren und dir die Claims anschauen. Im Anschluss führen wir ein GET-Request gegen den MS-Graph aus. Dank unserer Client-Konfiguration wird die Authentifizierung automatisch übernommen. Die URL des MS-Graphs steht in der Properties-Datei:
ms.graph.users.baserURI=https://graph.microsoft.com/v1.0/users/
Nun erstellen wir noch einen REST-Controller, um über das Frontend das Bild des Nutzers zu holen. Hierzu implementieren wir einen GET-Endpunkt, der ein Parameter für die Bildgröße entgegennimmt und als Antwort eine Byte-Array vom Media-Type „IMAGE_JPEG“ zurückgibt.
MsGraphApiController:
@RestController
@RequestMapping("/graph/")
public class MSGraphApiController {
@Autowired
private MsGraphService msGraphApiService;
@GetMapping(value = "picture/{size}", produces = MediaType.IMAGE_JPEG_VALUE)
@CrossOrigin
@ResponseStatus(HttpStatus.OK)
public byte[] getProfilePicture(@PathVariable String size) {
return msGraphApiService.getProfileImageForLoggedInUser(size);
}
Laden des Profilbildes im Frontend
Nun bleibt nur noch die Anbindung im Frontend. Dazu implementieren wir uns eine Pipe, die den Request gegen unser Backend ausführt und das Bild für uns als Data-URL einliest, so dass das Bild korrekt im HTML dargestellt werden kann.
Profile-image.pipe.ts:
@Pipe({
name: 'profileImage'
})
export class ProfileImagePipe implements PipeTransform {
constructor(private http: HttpClient, private authService: OAuthService) {
}
transform(url: string) {
const headers = new HttpHeaders({'Authorization': this.authService.getAccessToken(), 'Content-Type': 'image/*'});
return this.http.get(url, {headers: headers, responseType: 'blob'}).pipe(switchMap(blob => {
return new Observable((observer: Observer) => {
const reader = new FileReader();
reader.readAsDataURL(blob);
reader.onload = () => {
observer.next(reader.result)
};
})
}))
}
Die Pipe können wir jetzt in unserer „app-component.html“ verwenden. Zusätzlich geben wir hier noch den Namen des eingeloggten Nutzers an.
App-Component.html:
Hallo {{identityClaims.preferred_username}}
App-Component.ts:
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit{
title = 'azure-ad-demo-fe';
constructor(private oauhtService: OAuthService) {
}
ngOnInit(): void {
this.oauhtService.setupAutomaticSilentRefresh();
}
public get identityClaims(): any {
return this.oauhtService.getIdentityClaims();
}
}
Der Großteil sollte selbsterklärend sein. Die einzige Besonderheit hier ist das „setupAutomaticSilentRefresh“. Diese Methode sorgt dafür das der Access-Token erneuert wird, kurz bevor dieser ausläuft. Wenn wir jetzt die Angular-App starten, sollten wir nach erfolgreichem Login mit der Meldung „Hallo deine@mail.adresse“ und unserem Profilbild begrüßt werden.
Laden des Profilbilds
Ich hoffe ich konnte dir in diesem Beitrag näherbringen, wie du deine Applikation mit dem Azure AD absichern kannst und bedanke mich für deine Aufmerksamkeit. Dabei haben wir hier natürlich „nur“ an der Oberfläche gekratzt. Das OAuth-Thema ist recht umfangreich und bietet diverse Möglichkeiten deine Applikationen abzusichern. Für nähere Informationen: https://oauth.net/2/. Hat dir das geholfen, hast du Fragen oder Verbesserungsvorschläge? Dann freue ich mich auf deinen Kommentar!
Author