diff --git a/packages/swagger-ui-plugins/hooks/use-swagger-ui.hook.ts b/packages/swagger-ui-plugins/hooks/use-swagger-ui.hook.ts index bfb953e..d28a9f1 100644 --- a/packages/swagger-ui-plugins/hooks/use-swagger-ui.hook.ts +++ b/packages/swagger-ui-plugins/hooks/use-swagger-ui.hook.ts @@ -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); diff --git a/packages/swagger-ui-plugins/interfaces/System.ts b/packages/swagger-ui-plugins/interfaces/System.ts index d6cf7db..290fe77 100644 --- a/packages/swagger-ui-plugins/interfaces/System.ts +++ b/packages/swagger-ui-plugins/interfaces/System.ts @@ -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; @@ -15,7 +22,7 @@ export interface InitOAuthOptions { additionalQueryStringParams?: Record; useBasicAuthenticationWithAccessCodeGrant?: boolean; usePkceWithAuthorizationCodeGrant?: boolean; - allowedFlows?: string[]; + allowedFlows?: (string | Partial)[]; allowedScopes?: string[]; defaultSelectedScopes?: string[]; redirectUrl?: string; @@ -23,22 +30,30 @@ export interface InitOAuthOptions { } export interface AuthSelectors extends Record { - authorized(): Map>; shownDefinitions: () => List>>; + + authorized(): Map>; + definitionsToAuthorize(): Record; + definitionsForRequirements(security: string): Record; + getConfigs(): InitOAuthOptions; + setCurrentAuth(auth: string): void; + getCurrentAuth(): string; } export interface ErrSelectors extends Record { lastError(): any; + allErrors(): Iterable>; } export interface LayoutSelectors extends Record { currentFilter(): string | null | boolean | "false"; + currentAdvancedFilters(): Map; } @@ -48,18 +63,31 @@ export interface Oas3Selectors extends Record { export interface SpecSelectors extends Record { url(): string; + info(): Map; + loadingStatus(): string; + servers(): Map | undefined; + schemes(): Map | undefined; + securityDefinitions(): Map | undefined; + isSwagger2(): boolean; + isOAS3(): boolean; + isOAS31(): boolean; + specStr(): string; + operationsWithTags(): Map>>; + validOperationMethods(): List; + taggedOperations(): Map>; + security(): List>; } @@ -67,9 +95,14 @@ export interface SpecSelectors extends Record { * Actions */ export interface AuthActions extends Record { + logoutPopup: typeof logoutPopup; + showDefinitions(security: Record): void; + showDefinitions(show: false): void; + logoutWithPersistOption(auth: Record): void; + authPopup( authUrl: string, options: { @@ -80,7 +113,6 @@ export interface AuthActions extends Record { errCb: (err: Error) => void; } ): void; - logoutPopup: typeof logoutPopup; } export interface SpecActions extends Record { @@ -89,6 +121,7 @@ export interface SpecActions extends Record { export interface LayoutActions extends Record { updateFilter(value: string): void; + updateAdvancedFilters(value: Map): void; } diff --git a/packages/swagger-ui-plugins/plugins/auth/get-auth-name.util.ts b/packages/swagger-ui-plugins/plugins/auth/get-auth-name.util.ts index c81fcca..95723eb 100644 --- a/packages/swagger-ui-plugins/plugins/auth/get-auth-name.util.ts +++ b/packages/swagger-ui-plugins/plugins/auth/get-auth-name.util.ts @@ -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(); +} diff --git a/packages/swagger-ui-plugins/plugins/auth/oauth2.component.tsx b/packages/swagger-ui-plugins/plugins/auth/oauth2.component.tsx index fc8cae1..1e6559b 100644 --- a/packages/swagger-ui-plugins/plugins/auth/oauth2.component.tsx +++ b/packages/swagger-ui-plugins/plugins/auth/oauth2.component.tsx @@ -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 (
@@ -56,23 +59,19 @@ export function Oauth2Component(props: OAuth2Props) { Authorized
)} - {appName ? Application: : null} {description && (
)} - {oidcUrl && OpenID Connect URL:} {(flow === AUTH_FLOW_IMPLICIT || flow === AUTH_FLOW_ACCESS_CODE) && ( Authorization URL: )} - {(flow === AUTH_FLOW_PASSWORD || flow === AUTH_FLOW_ACCESS_CODE || flow === AUTH_FLOW_APPLICATION) && ( Token URL: )} - {!isAuthorized ? (
@@ -88,9 +87,7 @@ export function Oauth2Component(props: OAuth2Props) {
) : null} - {flow === AUTH_FLOW_PASSWORD ? : null} - {(!isAuthorized || (isAuthorized && clientId)) && (
{isAuthorized ? ( @@ -109,25 +106,26 @@ export function Oauth2Component(props: OAuth2Props) { )}
)} - - {!isPkceCodeGrant && (flow === AUTH_FLOW_APPLICATION || flow === AUTH_FLOW_ACCESS_CODE || flow === AUTH_FLOW_PASSWORD) && ( + {shouldShowSecret && (
{isAuthorized ? ( Client Secret: ) : ( - + <> + + )}
)} - {scopesOptions && scopesOptions.length ? ( !isAuthorized ? (
@@ -156,7 +154,6 @@ export function Oauth2Component(props: OAuth2Props) {
) : null ) : null} - {isAuthorized && accessToken ? ( ) : null} - {errors.valueSeq().map((error, key) => { return ; })} diff --git a/packages/swagger-ui-plugins/plugins/auth/use-flows.hook.tsx b/packages/swagger-ui-plugins/plugins/auth/use-flows.hook.tsx index 7a9295d..036e3b5 100644 --- a/packages/swagger-ui-plugins/plugins/auth/use-flows.hook.tsx +++ b/packages/swagger-ui-plugins/plugins/auth/use-flows.hook.tsx @@ -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>; + schemaName: string; + allowedFlows?: AllowedFlowOpts[]; } const FlowsLabels: Record = { @@ -13,41 +15,48 @@ const FlowsLabels: Record = { Implicit deprecated
), - authorization_code: "Authorization Code" + authorization_code: ( +
+ Authorization code using PCKE +
+ ), + client_credentials: ( +
+ Client credentials application token +
+ ) }; -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: ( -
- {FlowsLabels[name]} with PCKE -
- ), - 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 + ); + flows = flows.sort((a, b) => { - return authConfigs.allowedFlows!.indexOf(a.value) - authConfigs.allowedFlows!.indexOf(b.value); + return weights[a.value] - weights[b.value]; }); } @@ -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 { diff --git a/packages/swagger-ui-plugins/plugins/auth/use-oauth2.hook.ts b/packages/swagger-ui-plugins/plugins/auth/use-oauth2.hook.ts index 7550174..1816004 100644 --- a/packages/swagger-ui-plugins/plugins/auth/use-oauth2.hook.ts +++ b/packages/swagger-ui-plugins/plugins/auth/use-oauth2.hook.ts @@ -1,10 +1,10 @@ import { Map } from "immutable"; import { SyntheticEvent, useState } from "react"; -import { open } from "../../utils/open"; import { oauth2Authorize } from "./oauth2-authorize.util"; import { useFlows, UseFlowsOptions } from "./use-flows.hook"; import { useScopes } from "./use-scopes.hook"; +import { useAllowedFlows } from "./user-allowed-flows.hook"; export interface OAuth2Props extends UseFlowsOptions { authorized: Map>; @@ -12,9 +12,10 @@ export interface OAuth2Props extends UseFlowsOptions { } export function useOAuth2(props: OAuth2Props) { - let { authorized, authSelectors, specSelectors, errSelectors, schemaName, flows: flowsSchemes } = props; + let { authorized, authSelectors, errSelectors, schemaName, flows: flowsSchemes } = props; - const flowsProps = useFlows(props); + const allowedFlows = useAllowedFlows({ schemaName, authSelectors }); + const flowsProps = useFlows({ ...props, allowedFlows }); const { authConfigs, setFlow, schema } = flowsProps; const auth = authorized && authorized.get(schemaName); @@ -25,7 +26,15 @@ export function useOAuth2(props: OAuth2Props) { const errors = errSelectors.allErrors().filter((err) => err!.get("authId") === schemaName); const isValid = !errors.filter((err) => err!.get("source") === "validation").size; - const { scopesOptions, scopes, setScopes, selectScopes } = useScopes({ auth, schema, authConfigs }); + const { scopesOptions, scopes, setScopes, selectScopes } = useScopes({ + auth, + schema, + authConfigs, + authSelectors, + schemaName, + allowedFlows, + flow: flowsProps.flow + }); const [appName, setAppName] = useState(authConfigs.appName); @@ -46,6 +55,7 @@ export function useOAuth2(props: OAuth2Props) { }); function setState(name: string | undefined, value: Type) { + console.log(name, value); switch (name) { case "flow": setFlow(value); @@ -130,12 +140,14 @@ export function useOAuth2(props: OAuth2Props) { }; function checkIsValid() { + console.log(flowsProps.flow, clientId, clientSecret); switch (flowsProps.flow) { case "implicit": case "authorization_code": return clientId && scopes.length > 0; case "password": return clientId && scopes.length > 0 && username && password; + case "client_credentials": case "application": return clientId && clientSecret && scopes.length > 0; } diff --git a/packages/swagger-ui-plugins/plugins/auth/use-scopes.hook.ts b/packages/swagger-ui-plugins/plugins/auth/use-scopes.hook.ts index 86bdf38..58e7e02 100644 --- a/packages/swagger-ui-plugins/plugins/auth/use-scopes.hook.ts +++ b/packages/swagger-ui-plugins/plugins/auth/use-scopes.hook.ts @@ -1,18 +1,31 @@ import { Map } from "immutable"; import { useState } from "react"; -import type { InitOAuthOptions } from "../../interfaces/System"; +import { AllowedFlowOpts, AuthSelectors, InitOAuthOptions } from "../../interfaces/System"; +import { useAllowedFlows } from "./user-allowed-flows.hook"; export function useScopes({ schema, auth, - authConfigs + allowedFlows, + authConfigs, + flow }: { + flow: string; auth?: Map; schema: Map; + authSelectors: AuthSelectors; + schemaName: string; + allowedFlows?: AllowedFlowOpts[]; authConfigs: InitOAuthOptions; }) { - const { allowedScopes, defaultSelectedScopes = [] } = authConfigs; + const { defaultSelectedScopes = [] } = authConfigs; + + const allowedScopes = + allowedFlows && allowedFlows.length + ? allowedFlows.find((allowedFlow) => allowedFlow.flow === flow)?.scopes || authConfigs.allowedScopes + : undefined; + console.log(allowedScopes); let scopesOptions = [...(schema.get("allowedScopes")! || schema.get("scopes")!)].map(([scope, description]) => { return { scope, description }; }); diff --git a/packages/swagger-ui-plugins/plugins/auth/user-allowed-flows.hook.ts b/packages/swagger-ui-plugins/plugins/auth/user-allowed-flows.hook.ts new file mode 100644 index 0000000..09e3781 --- /dev/null +++ b/packages/swagger-ui-plugins/plugins/auth/user-allowed-flows.hook.ts @@ -0,0 +1,20 @@ +import { AllowedFlowOpts, AuthSelectors } from "../../interfaces/System"; +import { getOauthId } from "./get-auth-name.util"; + +interface UseAllowedFlowsProps { + schemaName: string; + authSelectors: AuthSelectors; +} + +export function useAllowedFlows({ schemaName, authSelectors }: UseAllowedFlowsProps) { + const authConfigs = authSelectors.getConfigs() || {}; + + if (authConfigs.allowedFlows) { + const allowedFlows = authConfigs.allowedFlows as AllowedFlowOpts[]; + const flowId = getOauthId(schemaName).toLowerCase(); + + return allowedFlows.filter(({ names }) => { + return names.length ? names.includes(flowId) : true; + }) as AllowedFlowOpts[]; + } +} diff --git a/packages/swagger-ui-v3/src/preset.config.ts b/packages/swagger-ui-v3/src/preset.config.ts index 2698ad9..0d4a6bf 100644 --- a/packages/swagger-ui-v3/src/preset.config.ts +++ b/packages/swagger-ui-v3/src/preset.config.ts @@ -4,7 +4,7 @@ export const presetConfig = { contact: "mailto:lvisdigiapi@clubmed.com", oauth: { usePkceWithAuthorizationCodeGrant: true, - allowedFlows: ["implicit", "authorization_code"], + allowedFlows: ["implicit", "authorization_code", { flow: "client_credentials", names: ["go"], scopes: ["pms"] }], allowedScopes: ["openid", "email", "profile", "api_admin"], defaultSelectedScopes: ["openid", "email", "profile"] },