I dette blogginnlegget skal vi se på hvordan man kan oppnå granulær tilgangsstyring med OBO-flyt og Entra ID.
Hva er OBO-flyt, og hvorfor skal man bruke det?
I OBO-flyt henter man ut access tokens på vegne av en bruker. Dette kan være et kraftig verktøy i flerlagstjenester med tilgangsstyring.

Figuren over illustrerer et slikt system, hvor Client, API A og API B har hver sin app-registrering i Entra ID. Klienten skal bruke API A til å hente data fra API B. API A autentiserer requests med Token A, og API B autentiserer requests med Token B. I tillegg til dette har Client også en token (heretter Token FE) som har blitt hentet etter innlogging.
Hva er egentlig forskjellen mellom de ulike tokenene? Audience. Hver app-registrering har et eget audience, og fungerer som en ID for app-registreringen. Token A må ha audience til app-registreringen til API A, og det samme gjelder Token B. Og siden man kan ha ulike roller i hver app-registrering kan Token A og Token B for en bruker ha forskjellige tilganger i API A og API B.
Hvorfor OBO-flyt?
OBO-flyt åpner opp for granulær tilgangskontroll ved å spesifisere nøyaktig hvilke tillatelser og områder en tjeneste kan be om på vegne av brukeren. Dette sikrer at tjenesten kun har tilgang til nødvendige ressurser. I tillegg er OBO-flyt ideell for flerlagstjenester der en front-end tjeneste kan kalle en back-end tjeneste på vegne av brukeren.
Systemet vi skal implementere💻
Vi skal nå implementere et enkelt system som henter ut brukere fra et API. Systemet består av en Backstage-app (Client) og et Kotlin REST API (API A). I Backstage-pluginen skal man kunne velge en rolle og se alle brukere med den rollen.

En forutsetning er at både Entra ID og app-registreringene er satt opp. Ta en titt her for å se hvordan app-registeringene må settes opp for OBO-flyt.
Kildekoden til klienten og backend API-et er lenket under:
Backstage🎬
Backstage er en open source utviklerportal utviklet av Spotify. Utviklerportalen er bygd opp av en rekke plugins. Man kan tilpasse Backstage til eget bruk, ved å utvikle plugins. Det er to typer plugins: frontend og backend. Førstnevnte er ansvarlig for brukerinteraksjon, og sistnevnte tar for seg kommunikasjon med serveren.
Vi ønsker å abstrahere bort OBO-flyten fra klienten. Dette kan gjøres ved å lage en backend-plugin, som vil fungere som en proxy. En fordel med dette er at man aldri ser Token A i nettverksloggen. I Backstage slipper man også CORS-problematikk dersom man utfører kall gjennom backend-pluginen.
Fra en brukers perspektiv blir det gjort et autentisert kall til proxyen, og data blir returnert tilbake. I realiteten blir det gjort langt flere kall. Alle de stiplede linjene er operasjoner brukeren ikke ser.

Steg 0: Entra ID og backend plugin
Vi kommer ikke til å ta for oss hvordan Entra ID er satt opp i Backstage, og backend-pluginen settes opp, men README-filen viser hva som har blitt gjort for kodebasen. Woodgrove har en stegvis gjennomgang av hvordan man setter opp Entra ID for OBO-flyt.
Steg 1: Sette opp services for Entra ID og proxy i backend plugin
Microsoft sitt MSAL-bibliotek kan brukes til å hente ut en token på vegne av brukeren. Etter å ha satt opp en ConfidentialClientApplication
kan man bruke metoden acquireTokenOnBehalfOf
. Som parametere gir man da en assertion og et scope. Assertion er brukerens Token FE. Scopet vårt er <API-A-client-id>/.default
hvor <API-A-client-id>
forteller MSAL hvilken app-registrering token skal hentes fra, og .default
forteller at den kun skal hente ut det som er definert som standard scope i API A sin app-registrering.
// plugins/obo-flow-plugin-backend/src/service/types.ts
export type EntraIdConfiguration = {
tenant_id: string;
client_id: string; // <Backstage-client-ID>
client_secret: string; // <Backstage-client-secret>
scope: string; // <API-A-audience>/.default
};
// plugins/obo-flow-plugin-backend/src/service/entraIdService.ts
export class EntraIdService {
private entraIdConfig: EntraIdConfiguration;
private clientApplication: ConfidentialClientApplication;
constructor(entraIdConfig: EntraIdConfiguration) {
this.entraIdConfig = entraIdConfig;
const msalConfig: Configuration = {
auth: {
clientId: this.entraIdConfig.client_id, // Backstage client ID
clientSecret: this.entraIdConfig.client_secret, // Backstage client secret
authority: `https://login.microsoftonline.com/${this.entraIdConfig.tenant_id}`,
},
};
this.clientApplication = new ConfidentialClientApplication(msalConfig);
}
async acquireTokenOnBehalfOfUser(token: string) {
const request: OnBehalfOfRequest = {
oboAssertion: token, // The assertion is the Token FE
scopes: [this.entraIdConfig.scope], // <API-A-audience>/.default
};
const response = await this.clientApplication.acquireTokenOnBehalfOf(
request,
);
return response?.accessToken; // The output is Token A
}
}
I proxyen har vi definert en GET-metode, som gjennomfører OBO-flyten før kallet til API A.
// plugins/obo-flow-plugin-backend/src/service/entraIdService.ts
export class ProxyService {
private readonly baseUrlApiA: string;
private entraIdService: EntraIdService;
constructor(baseUrlApiA: string, entraIdService: EntraIdService) {
this.baseUrlApiA = baseUrlApiA;
this.entraIdService = entraIdService;
}
async getData(
logger: LoggerService,
clientToken: string,
endpoint: string,
): Promise<any> {
const tokenA = await this.entraIdService.acquireTokenOnBehalfOfUser(
clientToken,
);
logger.info(`Proxy made a GET request to ${this.baseUrlApiA}/${endpoint}`);
if (!tokenA) return null;
const response = await fetch(`${this.baseUrlApiA}/${endpoint}`, {
headers: {
Accept: 'application/json',
Authorization: `Bearer ${tokenA}`,
},
method: 'GET',
});
return response.json();
}
}
Steg 2: Lage et proxy endepunkt
Nå som vi har definert services som håndterer OBO-flyten og proxy-kall, kan vi definere et endepunkt som kan nås av frontend-pluginen. Da backend-pluginen ble laget, skal en fil som heter router.ts ha blitt lagt til. Det er her man definerer API-et til pluginen. I metoden createRouter
må vi først hente ut miljøvariabler fra app-config.yaml
.
// plugins/obo-flow-plugin-backend/src/service/router.ts
const backendBaseUrl = config.getString('backend.backendBaseUrl');
const clientId = config.getString('msal.clientId');
const clientSecret = config.getString('msal.clientSecret');
const tenantId = config.getString('msal.tenantId');
const scope = `${config.getString('msal.clientIdApiA')}/.default`;
const entraIdConfiguration: EntraIdConfiguration = {
tenant_id: tenantId,
client_id: clientId,
client_secret: clientSecret,
scope: scope,
};
const entraIdService = new EntraIdService(entraIdConfiguration);
const proxyService = new ProxyService(backendBaseUrl, entraIdService);
Man kan nå bruke proxy-servicen til å sette opp proxy-endepunktet.
// plugins/obo-flow-plugin-backend/src/service/router.ts
export const oboFlowPluginPlugin = createBackendPlugin({
pluginId: 'obo-flow-plugin',
register(env) {
env.registerInit({
deps: {
httpRouter: coreServices.httpRouter,
logger: coreServices.logger,
config: coreServices.rootConfig,
},
async init({
httpRouter,
logger,
config,
}) {
httpRouter.use(
await createRouter({
logger,
config,
}),
);
httpRouter.addAuthPolicy({
path: '/proxy',
allow: 'unauthenticated',
});
},
});
},
});
Vi gjør proxy-endepunktet åpent for alle ved å bruke metoden httpRouter.addAuthPolicy
. Dette kan man gjøre av to grunner:
- For OBO-flyten må brukeren sende med en Entra-token som har samme audience som Backstage-app-registeringen
- For å kunne hente ut en slik token må brukeren være logget inn med sin Entra-bruker
// plugins/obo-flow-plugin-backend/src/plugin.ts
export const securityMetricBackendPlugin = createBackendPlugin({
pluginId: 'security-metric-backend',
register(env) {
env.registerInit({
deps: {
httpRouter: coreServices.httpRouter,
auth: coreServices.auth,
logger: coreServices.logger,
config: coreServices.rootConfig,
},
async init({ httpRouter, auth, logger, config }) {
httpRouter.use(
await createRouter({
auth,
logger,
config,
}),
);
httpRouter.addAuthPolicy({
path: '/proxy',
allow: 'unauthenticated',
});
},
});
},
});
Disse kodeblokkene er det man trenger for å sette opp en proxy som gjennomfører OBO-flyten.
Steg 3: Kalle proxyen fra frontend pluginen
I API A har vi definert endepunktet api/users/{userRole}
hvor userRole
er et heltall som representerer vanlige brukere, moderatorer eller administratorer.
Endepunktet vi definerte kan bli nådd ved å gjøre kall til localhost:7007/api/obo-flow-plugin/proxy
. Backstage backend kjører på port 7007
. Man kan gjøre kall til localhost:7007
fra localhost:3000
uten å møte på CORS problematikk.
For å hente ut variabler fra app-config.yaml
og Entra ID-token kan man henholdsvis bruke Backstages configApiRef
og microsoftAuthApiRef
.
const config = useApi(configApiRef);
const microsoftAuthApi = useApi(microsoftAuthApiRef);
const [userRole, setUserRle] = useState<UserRole>(UserRole.User);
const backendUrl = config.getString('backend.baseUrl');
const clientToken = await microsoftAuthApi.getAccessToken(
'backstage-client-id/.default',
);
const response = await fetch(`${baseUrl}/api/obo-flow-plugin/proxy`, {
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
method: 'POST',
body: JSON.stringify({
endpoint: `${endpoint}/${userRole.valueOf()}`,
}),
});
API A
Vi skal implementere et filter som autentiserer brukeren og konverterer et JWT til et OIDC token. Filteret vil trigges før alle requests til API A.
Steg 1: Legge til nødvendige dependencies
I build.gradle.kts må vi legge til følgende dependencies:
// build.gradle.kts
implementation("org.springframework.boot:spring-boot-starter-security")
implementation("org.springframework.boot:spring-boot-starter-oauth2-resource-server")
Steg 2: Konfigurasjon av OAuth 2.0 resource server
I application.yaml
må man definere hvilken issuer og hvilken audience (client ID) brukere skal autentiseres mot.
// src/main/resources/application.yaml
spring:
application:
name: medium-obo-flow-api-a
security:
oauth2:
resource-server:
jwt:
issuer-uri: https://sts.windows.net/<TENANT-ID>/
audiences: <CLIENT-ID-API-A>
Dette er alt som skal til for at API A skal kunne snakke med Entra ID.
Trinn 3: Oppsett av security filter chain med JWT og OIDC⛓️💥
I filen SecurityConfiguration skal vi sette opp to ting:
securityFilterChain
JwtToOidcConverter
Sistnevnte konverterer et JWT til et OIDC-token som videre kan brukes av Spring Boot.
// src/main/kotlin/com/example/mediumoboflowapia/configurations/SecurityConfiguration.kt
@Component
class JwtToOidcConverter : Converter<Jwt, AbstractAuthenticationToken> {
private val jwtGrantedAuthoritiesConverter = JwtGrantedAuthoritiesConverter()
override fun convert(jwt: Jwt): AbstractAuthenticationToken {
val authorities: Collection<GrantedAuthority> = jwtGrantedAuthoritiesConverter.convert(jwt) ?: emptyList()
val oidcUser: OidcUser = createOidcUser(jwt, authorities)
return OidcAuthenticationToken(oidcUser, authorities, jwt.tokenValue)
}
private fun createOidcUser(jwt: Jwt, authorities: Collection<GrantedAuthority>): OidcUser {
val claims = jwt.claims
val oidcIdToken = OidcIdToken(
jwt.tokenValue,
jwt.issuedAt ?: Instant.now(),
jwt.expiresAt ?: Instant.now().plusSeconds(3600),
claims
)
val oidcUserInfo = OidcUserInfo(claims)
return DefaultOidcUser(authorities, oidcIdToken, oidcUserInfo, "sub")
}
}
class OidcAuthenticationToken(
private val oidcUser: OidcUser,
authorities: Collection<GrantedAuthority>,
private val token: String
) : AbstractAuthenticationToken(authorities) {
init {
isAuthenticated = true
}
override fun getCredentials(): Any = token
override fun getPrincipal(): Any = oidcUser
}
securityFilterChain
er selve filteret, og bruker koden over. Her definerer vi regler for filteret. I koden nedenfor trenger man for eksempel ikke å være autentisert for å nå Swagger.
// src/main/kotlin/com/example/mediumoboflowapia/configurations/SecurityConfiguration.kt
@Bean
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
http.authorizeHttpRequests { requests ->
requests
.requestMatchers("/swagger-ui/**", "/v3/api-docs/**")
.permitAll()
.anyRequest().authenticated()
}.oauth2ResourceServer { oauth2 ->
oauth2
.jwt { jwtConfigurer ->
jwtConfigurer.jwtAuthenticationConverter(jwtToOidcConverter)
}
}
return http.build()
}
Avslutning👋
Du har nå satt opp en funksjonell OBO-flyt! Vi har kun sett på autentisering, men takket være OBO-flyten kan man ha granulær autorisasjon på brukernivå. Mulighetene er endeløse!
Del kunnskapen
Har du en kollega som også hadde dratt nytte av denne artikkelen?
Skrevet av
Relevant innhold
Her finner du innhold i samme gata om du vil lære mer.
Mer fra Fag i Bekk
Nå er du ved veis ende. Gå til forsiden hvis du vil ha mer faglig påfyll.
Til forsiden