Skip to content

Commit

Permalink
feat: adds authorization handler (#1575)
Browse files Browse the repository at this point in the history
* feat: adds authorization handler

* use getter in method

* fix: removes extraneous getter

---------

Co-authored-by: Vincent Biret <vibiret@microsoft.com>
  • Loading branch information
rkodev and baywet authored Jan 31, 2025
1 parent 58cf6c5 commit f9ff6ab
Show file tree
Hide file tree
Showing 5 changed files with 179 additions and 2 deletions.
1 change: 1 addition & 0 deletions packages/http/fetch/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
export * from "./fetchRequestAdapter";
export * from "./httpClient";
export * from "./middlewares/middleware";
export * from "./middlewares/authorizationHandler";
export * from "./middlewares/chaosHandler";
export * from "./middlewares/customFetchHandler";
export * from "./middlewares/compressionHandler";
Expand Down
8 changes: 7 additions & 1 deletion packages/http/fetch/src/kiotaClientFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@

import { Middleware, MiddlewareFactory } from ".";
import { HttpClient } from "./httpClient";
import { BaseBearerTokenAuthenticationProvider } from "@microsoft/kiota-abstractions";
import { AuthorizationHandler } from "./middlewares/authorizationHandler";

/**
*
Expand All @@ -21,6 +23,7 @@ export class KiotaClientFactory {
* If middlewares param is undefined, the httpClient instance will use the default array of middlewares.
* Set middlewares to `null` if you do not wish to use middlewares.
* If custom fetch is undefined, the httpClient instance uses the `DefaultFetchHandler`
* @param authenticationProvider - an optional instance of BaseBearerTokenAuthenticationProvider to be used for authentication
* @returns a HttpClient instance
* @example
* ```Typescript
Expand All @@ -44,8 +47,11 @@ export class KiotaClientFactory {
* ```
*/
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
public static create(customFetch: (request: string, init: RequestInit) => Promise<Response> = (...args) => fetch(...args) as any, middlewares?: Middleware[]): HttpClient {
public static create(customFetch: (request: string, init: RequestInit) => Promise<Response> = (...args) => fetch(...args) as any, middlewares?: Middleware[], authenticationProvider?: BaseBearerTokenAuthenticationProvider): HttpClient {
const middleware = middlewares || MiddlewareFactory.getDefaultMiddlewares(customFetch);
if (authenticationProvider) {
middleware.unshift(new AuthorizationHandler(authenticationProvider));
}
return new HttpClient(customFetch, ...middleware);
}
}
109 changes: 109 additions & 0 deletions packages/http/fetch/src/middlewares/authorizationHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/**
* -------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License.
* See License in the project root for license information.
* -------------------------------------------------------------------------------------------
*/

import { type RequestOption } from "@microsoft/kiota-abstractions";
import { Span, trace } from "@opentelemetry/api";

import { getObservabilityOptionsFromRequest } from "../observabilityOptions";
import type { Middleware } from "./middleware";
import type { FetchRequestInit } from "../utils/fetchDefinitions";
import { getRequestHeader, setRequestHeader } from "../utils/headersUtil";
import { BaseBearerTokenAuthenticationProvider } from "@microsoft/kiota-abstractions";

export class AuthorizationHandler implements Middleware {
next: Middleware | undefined;

/**
* A member holding the name of content range header
*/
private static readonly AUTHORIZATION_HEADER = "Authorization";

public constructor(private readonly authenticationProvider: BaseBearerTokenAuthenticationProvider) {
if (!authenticationProvider) {
throw new Error("authenticationProvider cannot be undefined");
}
}

public execute(url: string, requestInit: RequestInit, requestOptions?: Record<string, RequestOption>): Promise<Response> {
const obsOptions = getObservabilityOptionsFromRequest(requestOptions);
if (obsOptions) {
return trace.getTracer(obsOptions.getTracerInstrumentationName()).startActiveSpan("authorizationHandler - execute", (span) => {
try {
span.setAttribute("com.microsoft.kiota.handler.authorization.enable", true);
return this.executeInternal(url, requestInit as FetchRequestInit, requestOptions, span);
} finally {
span.end();
}
});
}
return this.executeInternal(url, requestInit as FetchRequestInit, requestOptions, undefined);
}

private async executeInternal(url: string, fetchRequestInit: FetchRequestInit, requestOptions?: Record<string, RequestOption>, span?: Span): Promise<Response> {
if (this.authorizationIsPresent(fetchRequestInit)) {
span?.setAttribute("com.microsoft.kiota.handler.authorization.token_present", true);
return await this.next!.execute(url, fetchRequestInit as RequestInit, requestOptions);
}

const token = await this.authenticateRequest(url);
setRequestHeader(fetchRequestInit, AuthorizationHandler.AUTHORIZATION_HEADER, `Bearer ${token}`);
const response = await this.next?.execute(url, fetchRequestInit as RequestInit, requestOptions);
if (!response) {
throw new Error("Response is undefined");
}
if (response.status !== 401) {
return response;
}
const claims = this.getClaimsFromResponse(response);
if (!claims) {
return response;
}
span?.addEvent("com.microsoft.kiota.handler.authorization.challenge_received");
const claimsToken = await this.authenticateRequest(url, claims);
setRequestHeader(fetchRequestInit, AuthorizationHandler.AUTHORIZATION_HEADER, `Bearer ${claimsToken}`);
span?.setAttribute("http.request.resend_count", 1);
const retryResponse = await this.next?.execute(url, fetchRequestInit as RequestInit, requestOptions);
if (!retryResponse) {
throw new Error("Response is undefined");
}
return retryResponse;
}

private authorizationIsPresent(request: FetchRequestInit | undefined): boolean {
if (!request) {
return false;
}
const authorizationHeader = getRequestHeader(request, AuthorizationHandler.AUTHORIZATION_HEADER);
return authorizationHeader !== undefined && authorizationHeader !== null;
}

private async authenticateRequest(url: string, claims?: string): Promise<string> {
const additionalAuthenticationContext = {} as Record<string, unknown>;
if (claims) {
additionalAuthenticationContext.claims = claims;
}
return await this.authenticationProvider.accessTokenProvider.getAuthorizationToken(url, additionalAuthenticationContext);
}

private readonly getClaimsFromResponse = (response: Response, claims?: string) => {
if (response.status === 401 && !claims) {
// avoid infinite loop, we only retry once
// no need to check for the content since it's an array and it doesn't need to be rewound
const rawAuthenticateHeader = response.headers.get("WWW-Authenticate");
if (rawAuthenticateHeader && /^Bearer /gi.test(rawAuthenticateHeader)) {
const rawParameters = rawAuthenticateHeader.replace(/^Bearer /gi, "").split(",");
for (const rawParameter of rawParameters) {
const trimmedParameter = rawParameter.trim();
if (/claims="[^"]+"/gi.test(trimmedParameter)) {
return trimmedParameter.replace(/claims="([^"]+)"/gi, "$1");
}
}
}
}
return undefined;
};
}
45 changes: 45 additions & 0 deletions packages/http/fetch/test/common/middleware/authorizationHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { beforeEach, describe, expect, it } from "vitest";
import { AuthorizationHandler } from "../../../src";
import { DummyFetchHandler } from "./dummyFetchHandler";
import { AccessTokenProvider, AllowedHostsValidator, BaseBearerTokenAuthenticationProvider } from "@microsoft/kiota-abstractions";

describe("AuthorizationHandler", () => {
let authorizationHandler: AuthorizationHandler;
let nextMiddleware: DummyFetchHandler;

beforeEach(() => {
nextMiddleware = new DummyFetchHandler();
const validator = new AllowedHostsValidator();
const tokenProvider: AccessTokenProvider = {
getAuthorizationToken: async () => "New Token",
getAllowedHostsValidator: () => validator,
};
const provider = new BaseBearerTokenAuthenticationProvider(tokenProvider);
authorizationHandler = new AuthorizationHandler(provider);
authorizationHandler.next = nextMiddleware;
});

it("should not add a header if authorization header exists", async () => {
const url = "https://example.com";
const requestInit = {
headers: {
Authorization: "Bearer Existing Token",
},
};
nextMiddleware.setResponses([new Response("ok", { status: 200 })]);

await authorizationHandler.execute(url, requestInit);

expect((requestInit.headers as Record<string, string>)["Authorization"]).toBe("Bearer Existing Token");
});

it("should attempt to authenticate when the header does not exist", async () => {
const url = "https://example.com";
const requestInit = { headers: {} };
nextMiddleware.setResponses([new Response("ok", { status: 200 })]);

await authorizationHandler.execute(url, requestInit);

expect((requestInit.headers as Record<string, string>)["Authorization"]).toBe("Bearer New Token");
});
});
18 changes: 17 additions & 1 deletion packages/http/fetch/test/node/kiotaClientFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
*/

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

describe("browser - KiotaClientFactory", () => {
it("Should return the http client", () => {
Expand Down Expand Up @@ -36,4 +37,19 @@ describe("browser - KiotaClientFactory", () => {
assert.isTrue(middleware?.next?.next?.next instanceof RedirectHandler);
assert.isTrue(middleware?.next?.next?.next?.next instanceof HeadersInspectionHandler);
});

it("Should add an AuthorizationHandler if authenticationProvider is defined ", () => {
const middlewares = [new UserAgentHandler(), new ParametersNameDecodingHandler(), new RetryHandler(), new RedirectHandler(), new HeadersInspectionHandler()];

const authenticationProvider = new BaseBearerTokenAuthenticationProvider({} as any);
const httpClient = KiotaClientFactory.create(undefined, middlewares, authenticationProvider);
assert.isDefined(httpClient);
assert.isDefined(httpClient["middleware"]);
const middleware = httpClient["middleware"];
assert.isTrue(middleware instanceof AuthorizationHandler);
assert.isTrue(middleware?.next instanceof UserAgentHandler);
assert.isTrue(middleware?.next?.next instanceof ParametersNameDecodingHandler);
assert.isTrue(middleware?.next?.next?.next instanceof RetryHandler);
assert.isTrue(middleware?.next?.next?.next?.next instanceof RedirectHandler);
});
});

0 comments on commit f9ff6ab

Please sign in to comment.