Skip to content

Commit 2af6c81

Browse files
committed
feat: adds authorization handler
1 parent 3868a28 commit 2af6c81

File tree

6 files changed

+181
-2
lines changed

6 files changed

+181
-2
lines changed

packages/abstractions/src/authentication/baseBearerTokenAuthenticationProvider.ts

+2
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,6 @@ export class BaseBearerTokenAuthenticationProvider implements AuthenticationProv
3535
}
3636
}
3737
};
38+
39+
public getAccessTokenProvider = () => this.accessTokenProvider;
3840
}

packages/http/fetch/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
export * from "./fetchRequestAdapter";
1212
export * from "./httpClient";
1313
export * from "./middlewares/middleware";
14+
export * from "./middlewares/authorizationHandler";
1415
export * from "./middlewares/chaosHandler";
1516
export * from "./middlewares/customFetchHandler";
1617
export * from "./middlewares/compressionHandler";

packages/http/fetch/src/kiotaClientFactory.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77

88
import { Middleware, MiddlewareFactory } from ".";
99
import { HttpClient } from "./httpClient";
10+
import { BaseBearerTokenAuthenticationProvider } from "@microsoft/kiota-abstractions";
11+
import { AuthorizationHandler } from "./middlewares/authorizationHandler";
1012

1113
/**
1214
*
@@ -21,6 +23,7 @@ export class KiotaClientFactory {
2123
* If middlewares param is undefined, the httpClient instance will use the default array of middlewares.
2224
* Set middlewares to `null` if you do not wish to use middlewares.
2325
* If custom fetch is undefined, the httpClient instance uses the `DefaultFetchHandler`
26+
* @param authenticationProvider - an optional instance of BaseBearerTokenAuthenticationProvider to be used for authentication
2427
* @returns a HttpClient instance
2528
* @example
2629
* ```Typescript
@@ -44,8 +47,11 @@ export class KiotaClientFactory {
4447
* ```
4548
*/
4649
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
47-
public static create(customFetch: (request: string, init: RequestInit) => Promise<Response> = (...args) => fetch(...args) as any, middlewares?: Middleware[]): HttpClient {
50+
public static create(customFetch: (request: string, init: RequestInit) => Promise<Response> = (...args) => fetch(...args) as any, middlewares?: Middleware[], authenticationProvider?: BaseBearerTokenAuthenticationProvider): HttpClient {
4851
const middleware = middlewares || MiddlewareFactory.getDefaultMiddlewares(customFetch);
52+
if (authenticationProvider) {
53+
middleware.unshift(new AuthorizationHandler(authenticationProvider));
54+
}
4955
return new HttpClient(customFetch, ...middleware);
5056
}
5157
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
/**
2+
* -------------------------------------------------------------------------------------------
3+
* Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License.
4+
* See License in the project root for license information.
5+
* -------------------------------------------------------------------------------------------
6+
*/
7+
8+
import { type RequestOption } from "@microsoft/kiota-abstractions";
9+
import { Span, trace } from "@opentelemetry/api";
10+
11+
import { getObservabilityOptionsFromRequest } from "../observabilityOptions";
12+
import type { Middleware } from "./middleware";
13+
import type { FetchRequestInit } from "../utils/fetchDefinitions";
14+
import { getRequestHeader, setRequestHeader } from "../utils/headersUtil";
15+
import { BaseBearerTokenAuthenticationProvider } from "@microsoft/kiota-abstractions";
16+
17+
export class AuthorizationHandler implements Middleware {
18+
next: Middleware | undefined;
19+
20+
/**
21+
* A member holding the name of content range header
22+
*/
23+
private static readonly AUTHORIZATION_HEADER = "Authorization";
24+
25+
public constructor(private readonly authenticationProvider: BaseBearerTokenAuthenticationProvider) {
26+
if (!authenticationProvider) {
27+
throw new Error("authenticationProvider cannot be undefined");
28+
}
29+
}
30+
31+
public execute(url: string, requestInit: RequestInit, requestOptions?: Record<string, RequestOption>): Promise<Response> {
32+
const obsOptions = getObservabilityOptionsFromRequest(requestOptions);
33+
if (obsOptions) {
34+
return trace.getTracer(obsOptions.getTracerInstrumentationName()).startActiveSpan("authorizationHandler - execute", (span) => {
35+
try {
36+
span.setAttribute("com.microsoft.kiota.handler.authorization.enable", true);
37+
return this.executeInternal(url, requestInit as FetchRequestInit, requestOptions, span);
38+
} finally {
39+
span.end();
40+
}
41+
});
42+
}
43+
return this.executeInternal(url, requestInit as FetchRequestInit, requestOptions, undefined);
44+
}
45+
46+
private async executeInternal(url: string, fetchRequestInit: FetchRequestInit, requestOptions?: Record<string, RequestOption>, span?: Span): Promise<Response> {
47+
if (this.authorizationIsPresent(fetchRequestInit)) {
48+
span?.setAttribute("com.microsoft.kiota.handler.authorization.token_present", true);
49+
return await this.next!.execute(url, fetchRequestInit as RequestInit, requestOptions);
50+
}
51+
52+
const token = await this.authenticateRequest(url);
53+
setRequestHeader(fetchRequestInit, AuthorizationHandler.AUTHORIZATION_HEADER, `Bearer ${token}`);
54+
const response = await this.next?.execute(url, fetchRequestInit as RequestInit, requestOptions);
55+
if (!response) {
56+
throw new Error("Response is undefined");
57+
}
58+
if (response.status !== 401) {
59+
return response;
60+
}
61+
const claims = this.getClaimsFromResponse(response);
62+
if (!claims) {
63+
return response;
64+
}
65+
span?.addEvent("com.microsoft.kiota.handler.authorization.challenge_received");
66+
const claimsToken = await this.authenticateRequest(url, claims);
67+
setRequestHeader(fetchRequestInit, AuthorizationHandler.AUTHORIZATION_HEADER, `Bearer ${claimsToken}`);
68+
span?.setAttribute("http.request.resend_count", 1);
69+
const retryResponse = await this.next?.execute(url, fetchRequestInit as RequestInit, requestOptions);
70+
if (!retryResponse) {
71+
throw new Error("Response is undefined");
72+
}
73+
return retryResponse;
74+
}
75+
76+
private authorizationIsPresent(request: FetchRequestInit | undefined): boolean {
77+
if (!request) {
78+
return false;
79+
}
80+
const authorizationHeader = getRequestHeader(request, AuthorizationHandler.AUTHORIZATION_HEADER);
81+
return authorizationHeader !== undefined && authorizationHeader !== null;
82+
}
83+
84+
private async authenticateRequest(url: string, claims?: string): Promise<string> {
85+
const additionalAuthenticationContext = {} as Record<string, unknown>;
86+
if (claims) {
87+
additionalAuthenticationContext.claims = claims;
88+
}
89+
return await this.authenticationProvider.getAccessTokenProvider().getAuthorizationToken(url, additionalAuthenticationContext);
90+
}
91+
92+
private readonly getClaimsFromResponse = (response: Response, claims?: string) => {
93+
if (response.status === 401 && !claims) {
94+
// avoid infinite loop, we only retry once
95+
// no need to check for the content since it's an array and it doesn't need to be rewound
96+
const rawAuthenticateHeader = response.headers.get("WWW-Authenticate");
97+
if (rawAuthenticateHeader && /^Bearer /gi.test(rawAuthenticateHeader)) {
98+
const rawParameters = rawAuthenticateHeader.replace(/^Bearer /gi, "").split(",");
99+
for (const rawParameter of rawParameters) {
100+
const trimmedParameter = rawParameter.trim();
101+
if (/claims="[^"]+"/gi.test(trimmedParameter)) {
102+
return trimmedParameter.replace(/claims="([^"]+)"/gi, "$1");
103+
}
104+
}
105+
}
106+
}
107+
return undefined;
108+
};
109+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { beforeEach, describe, expect, it } from "vitest";
2+
import { AuthorizationHandler } from "../../../src";
3+
import { DummyFetchHandler } from "./dummyFetchHandler";
4+
import { AccessTokenProvider, AllowedHostsValidator, BaseBearerTokenAuthenticationProvider } from "@microsoft/kiota-abstractions";
5+
6+
describe("AuthorizationHandler", () => {
7+
let authorizationHandler: AuthorizationHandler;
8+
let nextMiddleware: DummyFetchHandler;
9+
10+
beforeEach(() => {
11+
nextMiddleware = new DummyFetchHandler();
12+
const validator = new AllowedHostsValidator();
13+
const tokenProvider: AccessTokenProvider = {
14+
getAuthorizationToken: async () => "New Token",
15+
getAllowedHostsValidator: () => validator,
16+
};
17+
const provider = new BaseBearerTokenAuthenticationProvider(tokenProvider);
18+
authorizationHandler = new AuthorizationHandler(provider);
19+
authorizationHandler.next = nextMiddleware;
20+
});
21+
22+
it("should not add a header if authorization header exists", async () => {
23+
const url = "https://example.com";
24+
const requestInit = {
25+
headers: {
26+
Authorization: "Bearer Existing Token",
27+
},
28+
};
29+
nextMiddleware.setResponses([new Response("ok", { status: 200 })]);
30+
31+
await authorizationHandler.execute(url, requestInit);
32+
33+
expect((requestInit.headers as Record<string, string>)["Authorization"]).toBe("Bearer Existing Token");
34+
});
35+
36+
it("should attempt to authenticate when the header does not exist", async () => {
37+
const url = "https://example.com";
38+
const requestInit = { headers: {} };
39+
nextMiddleware.setResponses([new Response("ok", { status: 200 })]);
40+
41+
await authorizationHandler.execute(url, requestInit);
42+
43+
expect((requestInit.headers as Record<string, string>)["Authorization"]).toBe("Bearer New Token");
44+
});
45+
});

packages/http/fetch/test/node/kiotaClientFactory.ts

+17-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
*/
77

88
import { assert, describe, it } from "vitest";
9-
import { CustomFetchHandler, HeadersInspectionHandler, KiotaClientFactory, ParametersNameDecodingHandler, RedirectHandler, RetryHandler, UrlReplaceHandler, UserAgentHandler } from "../../src";
9+
import { CustomFetchHandler, HeadersInspectionHandler, KiotaClientFactory, ParametersNameDecodingHandler, RedirectHandler, RetryHandler, UrlReplaceHandler, UserAgentHandler, AuthorizationHandler } from "../../src";
10+
import { BaseBearerTokenAuthenticationProvider } from "@microsoft/kiota-abstractions";
1011

1112
describe("browser - KiotaClientFactory", () => {
1213
it("Should return the http client", () => {
@@ -36,4 +37,19 @@ describe("browser - KiotaClientFactory", () => {
3637
assert.isTrue(middleware?.next?.next?.next instanceof RedirectHandler);
3738
assert.isTrue(middleware?.next?.next?.next?.next instanceof HeadersInspectionHandler);
3839
});
40+
41+
it("Should add an AuthorizationHandler if authenticationProvider is defined ", () => {
42+
const middlewares = [new UserAgentHandler(), new ParametersNameDecodingHandler(), new RetryHandler(), new RedirectHandler(), new HeadersInspectionHandler()];
43+
44+
const authenticationProvider = new BaseBearerTokenAuthenticationProvider({} as any);
45+
const httpClient = KiotaClientFactory.create(undefined, middlewares, authenticationProvider);
46+
assert.isDefined(httpClient);
47+
assert.isDefined(httpClient["middleware"]);
48+
const middleware = httpClient["middleware"];
49+
assert.isTrue(middleware instanceof AuthorizationHandler);
50+
assert.isTrue(middleware?.next instanceof UserAgentHandler);
51+
assert.isTrue(middleware?.next?.next instanceof ParametersNameDecodingHandler);
52+
assert.isTrue(middleware?.next?.next?.next instanceof RetryHandler);
53+
assert.isTrue(middleware?.next?.next?.next?.next instanceof RedirectHandler);
54+
});
3955
});

0 commit comments

Comments
 (0)