Skip to content

Commit 4614ce1

Browse files
committed
feat: adds authorization handler
1 parent 3868a28 commit 4614ce1

File tree

2 files changed

+185
-0
lines changed

2 files changed

+185
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
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 { FetchHeadersInit, FetchRequestInit } from "../utils/fetchDefinitions";
14+
import { getRequestHeader, setRequestHeader } from "../utils/headersUtil";
15+
import { BaseBearerTokenAuthenticationProvider, Headers, RequestInformation } from "@microsoft/kiota-abstractions/src";
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 requestInformation = new RequestInformation();
53+
requestInformation.URL = url;
54+
requestInformation.headers = new Headers();
55+
if (fetchRequestInit.headers !== undefined) {
56+
const headers = fetchRequestInit.headers as Record<string, string>;
57+
for (const key of Object.keys(headers)) {
58+
const value = headers[key];
59+
requestInformation.headers.add(key, value);
60+
}
61+
}
62+
63+
// add all the headers to fetchRequestInit
64+
if (!fetchRequestInit.headers) {
65+
fetchRequestInit.headers = {};
66+
}
67+
68+
await this.authenticateRequest(requestInformation);
69+
this.setAuthorizationHeaderFromRequestInformation(fetchRequestInit, requestInformation);
70+
const response = await this.next?.execute(url, fetchRequestInit as RequestInit, requestOptions);
71+
if (!response) {
72+
throw new Error("Response is undefined");
73+
}
74+
if (response.status !== 401) {
75+
return response;
76+
}
77+
const claims = this.getClaimsFromResponse(response);
78+
if (!claims) {
79+
return response;
80+
}
81+
span?.addEvent("com.microsoft.kiota.handler.authorization.challenge_received");
82+
await this.authenticateRequest(requestInformation, claims);
83+
this.setAuthorizationHeaderFromRequestInformation(fetchRequestInit, requestInformation);
84+
span?.setAttribute("http.request.resend_count", 1);
85+
const retryResponse = await this.next?.execute(url, fetchRequestInit as RequestInit, requestOptions);
86+
if (!retryResponse) {
87+
throw new Error("Response is undefined");
88+
}
89+
return retryResponse;
90+
}
91+
92+
private setAuthorizationHeaderFromRequestInformation(fetchRequestInit: FetchRequestInit, requestInformation: RequestInformation) {
93+
// get header from requestInformation
94+
let headerVal: string | undefined;
95+
if (requestInformation.headers) {
96+
const headerSet = requestInformation.headers.get(AuthorizationHandler.AUTHORIZATION_HEADER);
97+
if (headerSet && headerSet.size > 0) {
98+
headerVal = headerSet.values().next().value;
99+
}
100+
}
101+
if (headerVal) {
102+
setRequestHeader(fetchRequestInit, AuthorizationHandler.AUTHORIZATION_HEADER, headerVal);
103+
}
104+
}
105+
106+
private authorizationIsPresent(request: FetchRequestInit | undefined): boolean {
107+
if (!request) {
108+
return false;
109+
}
110+
const authorizationHeader = getRequestHeader(request, AuthorizationHandler.AUTHORIZATION_HEADER);
111+
return authorizationHeader !== undefined && authorizationHeader !== null;
112+
}
113+
114+
private async authenticateRequest(request: RequestInformation, claims?: string): Promise<void> {
115+
const additionalAuthenticationContext = {} as Record<string, unknown>;
116+
if (claims) {
117+
additionalAuthenticationContext.claims = claims;
118+
}
119+
await this.authenticationProvider.authenticateRequest(request, additionalAuthenticationContext);
120+
}
121+
122+
private readonly getClaimsFromResponse = (response: Response, claims?: string) => {
123+
if (response.status === 401 && !claims) {
124+
// avoid infinite loop, we only retry once
125+
// no need to check for the content since it's an array and it doesn't need to be rewound
126+
const rawAuthenticateHeader = response.headers.get("WWW-Authenticate");
127+
if (rawAuthenticateHeader && /^Bearer /gi.test(rawAuthenticateHeader)) {
128+
const rawParameters = rawAuthenticateHeader.replace(/^Bearer /gi, "").split(",");
129+
for (const rawParameter of rawParameters) {
130+
const trimmedParameter = rawParameter.trim();
131+
if (/claims="[^"]+"/gi.test(trimmedParameter)) {
132+
return trimmedParameter.replace(/claims="([^"]+)"/gi, "$1");
133+
}
134+
}
135+
}
136+
}
137+
return undefined;
138+
};
139+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { beforeEach, describe, expect, it } from "vitest";
2+
import { AuthorizationHandler } from "../../../src/middlewares/authorizationHandler";
3+
import { DummyFetchHandler } from "./dummyFetchHandler";
4+
import { AccessTokenProvider, AllowedHostsValidator, BaseBearerTokenAuthenticationProvider } from "@microsoft/kiota-abstractions/src";
5+
import { CompressionHandler } from "../../../src";
6+
7+
describe("AuthorizationHandler", () => {
8+
let authorizationHandler: AuthorizationHandler;
9+
let nextMiddleware: DummyFetchHandler;
10+
11+
beforeEach(() => {
12+
nextMiddleware = new DummyFetchHandler();
13+
const validator = new AllowedHostsValidator();
14+
const tokenProvider: AccessTokenProvider = {
15+
getAuthorizationToken: async () => "New Token",
16+
getAllowedHostsValidator: () => validator,
17+
};
18+
const provider = new BaseBearerTokenAuthenticationProvider(tokenProvider);
19+
authorizationHandler = new AuthorizationHandler(provider);
20+
authorizationHandler.next = nextMiddleware;
21+
});
22+
23+
it("should not add a header if authorization header exists", async () => {
24+
const url = "https://example.com";
25+
const requestInit = {
26+
headers: {
27+
Authorization: "Bearer Existing Token",
28+
},
29+
};
30+
nextMiddleware.setResponses([new Response("ok", { status: 200 })]);
31+
32+
await authorizationHandler.execute(url, requestInit);
33+
34+
expect((requestInit.headers as Record<string, string>)["Authorization"]).toBe("Bearer Existing Token");
35+
});
36+
37+
it("should attempt to authenticate when the header does not exist", async () => {
38+
const url = "https://example.com";
39+
const requestInit = { headers: {} };
40+
nextMiddleware.setResponses([new Response("ok", { status: 200 })]);
41+
42+
await authorizationHandler.execute(url, requestInit);
43+
44+
expect((requestInit.headers as Record<string, string>)["Authorization"]).toBe("Bearer New Token");
45+
});
46+
});

0 commit comments

Comments
 (0)