Skip to content

Commit

Permalink
feat(swagger): enable client_credentials flow for a GO auth
Browse files Browse the repository at this point in the history
  • Loading branch information
Romakita committed Sep 4, 2024
1 parent a51da6c commit af8b58e
Show file tree
Hide file tree
Showing 9 changed files with 163 additions and 52 deletions.
19 changes: 19 additions & 0 deletions packages/swagger-ui-plugins/hooks/use-swagger-ui.hook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,25 @@ export function useSwaggerUI(baseOpts: UseSwaggerUIOptions): SwaggerUIProps {
.filter(Boolean) as any[];
}

if (config.oauth?.allowedFlows) {
config.oauth!.allowedFlows = config.oauth.allowedFlows.map((item, index) => {
if (typeof item === "string") {
return {
id: index,
flow: item,
names: [],
scopes: config.oauth!.allowedScopes || []
};
}

item.id = index;
item.names = item.names || [];
item.scopes = item.scopes || config.oauth!.allowedScopes || [];

return item;
});
}

function onComplete(system: System) {
if (config.oauth) {
system.initOAuth(config.oauth);
Expand Down
39 changes: 36 additions & 3 deletions packages/swagger-ui-plugins/interfaces/System.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ import { SwaggerUIProps } from "swagger-ui-react";
import type { logoutPopup } from "../plugins/auth/actions/auth-popup.action";
import type { opsAdvancedFilter, opsFilter } from "../plugins/filter/ops-filter";

export interface AllowedFlowOpts {
id: number;
flow: string;
names: string[];
scopes: string[];
}

export interface InitOAuthOptions {
clientId?: string;
clientSecret?: string;
Expand All @@ -15,30 +22,38 @@ export interface InitOAuthOptions {
additionalQueryStringParams?: Record<string, string>;
useBasicAuthenticationWithAccessCodeGrant?: boolean;
usePkceWithAuthorizationCodeGrant?: boolean;
allowedFlows?: string[];
allowedFlows?: (string | Partial<AllowedFlowOpts>)[];
allowedScopes?: string[];
defaultSelectedScopes?: string[];
redirectUrl?: string;
postLogoutRedirectUrl?: string;
}

export interface AuthSelectors extends Record<string, unknown> {
authorized(): Map<string, Map<string, unknown>>;
shownDefinitions: () => List<Map<string, Map<string, any>>>;

authorized(): Map<string, Map<string, unknown>>;

definitionsToAuthorize(): Record<string, any>;

definitionsForRequirements(security: string): Record<string, any>;

getConfigs(): InitOAuthOptions;

setCurrentAuth(auth: string): void;

getCurrentAuth(): string;
}

export interface ErrSelectors extends Record<string, any> {
lastError(): any;

allErrors(): Iterable<string, Map<string, any>>;
}

export interface LayoutSelectors extends Record<string, any> {
currentFilter(): string | null | boolean | "false";

currentAdvancedFilters(): Map<string, any>;
}

Expand All @@ -48,28 +63,46 @@ export interface Oas3Selectors extends Record<string, any> {

export interface SpecSelectors extends Record<string, unknown> {
url(): string;

info(): Map<string, unknown>;

loadingStatus(): string;

servers(): Map<string, any> | undefined;

schemes(): Map<string, any> | undefined;

securityDefinitions(): Map<string, any> | undefined;

isSwagger2(): boolean;

isOAS3(): boolean;

isOAS31(): boolean;

specStr(): string;

operationsWithTags(): Map<string, OrderedMap<string, Map<string, unknown>>>;

validOperationMethods(): List<string>;

taggedOperations(): Map<string, Map<string, unknown>>;

security(): List<Map<string, any>>;
}

/**
* Actions
*/
export interface AuthActions extends Record<string, any> {
logoutPopup: typeof logoutPopup;

showDefinitions(security: Record<string, any>): void;

showDefinitions(show: false): void;

logoutWithPersistOption(auth: Record<string, any>): void;

authPopup(
authUrl: string,
options: {
Expand All @@ -80,7 +113,6 @@ export interface AuthActions extends Record<string, any> {
errCb: (err: Error) => void;
}
): void;
logoutPopup: typeof logoutPopup;
}

export interface SpecActions extends Record<string, any> {
Expand All @@ -89,6 +121,7 @@ export interface SpecActions extends Record<string, any> {

export interface LayoutActions extends Record<string, any> {
updateFilter(value: string): void;

updateAdvancedFilters(value: Map<string, any>): void;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,11 @@ export function getOauthName(name: string) {

return name;
}

export function getOauthId(name: string) {
if (name.indexOf("Bearer_") > -1) {
return name.split(" (")[0].replace("Bearer_", "").toLowerCase();
}

return name.toLowerCase();
}
36 changes: 16 additions & 20 deletions packages/swagger-ui-plugins/plugins/auth/oauth2.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ export function Oauth2Component(props: OAuth2Props) {

let description = schema.get("description");

const shouldShowSecret =
flow === AUTH_FLOW_APPLICATION || (!isPkceCodeGrant && (flow === AUTH_FLOW_ACCESS_CODE || flow === AUTH_FLOW_PASSWORD));

return (
<div className={"flex flex-col"}>
<div className={"mb-24 max-w-[718px]"}>
Expand All @@ -56,23 +59,19 @@ export function Oauth2Component(props: OAuth2Props) {
Authorized
</div>
)}

{appName ? <DisabledFieldComponent value={appName}>Application:</DisabledFieldComponent> : null}
{description && (
<div className="text-serif mb-12 py-4 text-b4">
<Markdown source={"test\nhello world"} />
</div>
)}

{oidcUrl && <DisabledFieldComponent value={oidcUrl}>OpenID Connect URL:</DisabledFieldComponent>}
{(flow === AUTH_FLOW_IMPLICIT || flow === AUTH_FLOW_ACCESS_CODE) && (
<DisabledFieldComponent value={schema.get("authorizationUrl")}>Authorization URL:</DisabledFieldComponent>
)}

{(flow === AUTH_FLOW_PASSWORD || flow === AUTH_FLOW_ACCESS_CODE || flow === AUTH_FLOW_APPLICATION) && (
<DisabledFieldComponent value={schema.get("tokenUrl")}>Token URL:</DisabledFieldComponent>
)}

{!isAuthorized ? (
<div className="bg-lightSand py-16 rounded-16 mb-16">
<FormControl label={"Select a flow:"} id={"flow"}>
Expand All @@ -88,9 +87,7 @@ export function Oauth2Component(props: OAuth2Props) {
</FormControl>
</div>
) : null}

{flow === AUTH_FLOW_PASSWORD ? <Oauth2PasswordFlow {...hookProps} /> : null}

{(!isAuthorized || (isAuthorized && clientId)) && (
<div className="mb-16">
{isAuthorized ? (
Expand All @@ -109,25 +106,26 @@ export function Oauth2Component(props: OAuth2Props) {
)}
</div>
)}

{!isPkceCodeGrant && (flow === AUTH_FLOW_APPLICATION || flow === AUTH_FLOW_ACCESS_CODE || flow === AUTH_FLOW_PASSWORD) && (
{shouldShowSecret && (
<div className="mb-16">
{isAuthorized ? (
<DisabledFieldComponent>Client Secret:</DisabledFieldComponent>
) : (
<Password
label="Client Secret:"
id={`client_secret_${flow}`}
value={clientSecret}
type="password"
data-name="clientSecret"
required={true}
onChange={setState}
/>
<>
<Password
label="Client Secret:"
id={`client_secret_${flow}`}
value={clientSecret}
type="password"
data-name="clientSecret"
name={"client_secret"}
required={true}
onChange={setState}
/>
</>
)}
</div>
)}

{scopesOptions && scopesOptions.length ? (
!isAuthorized ? (
<div className="bg-lightSand py-16 rounded-16 mb-16 relative">
Expand Down Expand Up @@ -156,7 +154,6 @@ export function Oauth2Component(props: OAuth2Props) {
</div>
) : null
) : null}

{isAuthorized && accessToken ? (
<DisabledFieldComponent
copy={accessToken}
Expand All @@ -166,7 +163,6 @@ export function Oauth2Component(props: OAuth2Props) {
Access Token:
</DisabledFieldComponent>
) : null}

{errors.valueSeq().map((error, key) => {
return <AuthError error={error} key={key} />;
})}
Expand Down
52 changes: 31 additions & 21 deletions packages/swagger-ui-plugins/plugins/auth/use-flows.hook.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { useLocalStorage } from "@clubmed/ui/hooks/storage/useLocaleStorage";
import { List, Map } from "immutable";

import { System } from "../../interfaces/System";
import { AllowedFlowOpts, System } from "../../interfaces/System";

export interface UseFlowsOptions extends System {
flows: List<Map<string, any>>;
schemaName: string;
allowedFlows?: AllowedFlowOpts[];
}

const FlowsLabels: Record<string, any> = {
Expand All @@ -13,41 +15,48 @@ const FlowsLabels: Record<string, any> = {
<span className="line-through">Implicit</span> <small className={"block text-gray"}>deprecated</small>
</div>
),
authorization_code: "Authorization Code"
authorization_code: (
<div>
Authorization code <small className={"block text-gray"}>using PCKE</small>
</div>
),
client_credentials: (
<div>
Client credentials <small className={"block text-gray"}>application token</small>
</div>
)
};

export function useFlows({ flows: flowsSchemes, specSelectors, authSelectors }: UseFlowsOptions) {
export function useFlows({ flows: flowsSchemes, specSelectors, authSelectors, schemaName, allowedFlows }: UseFlowsOptions) {
const authConfigs = authSelectors.getConfigs() || {};
const { value: flow, setItem: setFlow } = useLocalStorage(
"preferred_flow",
authConfigs.allowedFlows ? authConfigs.allowedFlows[0] : flowsSchemes.first().get("flow")
);
const defaultFlow = allowedFlows && allowedFlows.length ? allowedFlows[0].flow : flowsSchemes.first().get("flow");

const { value: flow, setItem: setFlow } = useLocalStorage("preferred_flow", defaultFlow);

const isPkceCodeGrant = !!authConfigs.usePkceWithAuthorizationCodeGrant;

let flows: { label: string; value: string }[] = flowsSchemes
.filter((schema) => (authConfigs.allowedFlows ? authConfigs.allowedFlows.includes(schema!.get("flow")) : true))
.filter((schema) => (allowedFlows ? allowedFlows.some(({ flow }) => flow === schema!.get("flow")) : true))
.map((schema) => {
const name = schema!.get("flow");

if (name === "authorization_code" && isPkceCodeGrant) {
return {
label: (
<div>
{FlowsLabels[name]} <small className={"block text-gray"}>with PCKE</small>
</div>
),
value: name
};
}

return { label: FlowsLabels[name], value: name };
})
.toArray();

if (authConfigs.allowedFlows) {
if (allowedFlows) {
const weights = allowedFlows.reduce(
(acc, { flow, id }) => {
return {
...acc,
[flow]: id
};
},
{} as Record<string, number>
);

flows = flows.sort((a, b) => {
return authConfigs.allowedFlows!.indexOf(a.value) - authConfigs.allowedFlows!.indexOf(b.value);
return weights[a.value] - weights[b.value];
});
}

Expand All @@ -62,6 +71,7 @@ export function useFlows({ flows: flowsSchemes, specSelectors, authSelectors }:
const AUTH_FLOW_PASSWORD = "password";
const AUTH_FLOW_ACCESS_CODE = isOAS3() ? (oidcUrl ? "authorization_code" : "authorizationCode") : "accessCode";
const AUTH_FLOW_APPLICATION = isOAS3() ? (oidcUrl ? "client_credentials" : "clientCredentials") : "application";

const flowToDisplay = flow === AUTH_FLOW_ACCESS_CODE && isPkceCodeGrant ? flow + " with PKCE" : flow;

return {
Expand Down
Loading

0 comments on commit af8b58e

Please sign in to comment.