-
Notifications
You must be signed in to change notification settings - Fork 2
feat(oidc-client): implement authorize API #344
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
introduce the oidc-client with the authorize api. authoirize handles the calling of authorize routes based on the well-known config. when a p1 env is detected, it will use a post request otherwise, a background request is done via a hidden iframe
|
View your CI Pipeline Execution ↗ for commit f35d74a
☁️ Nx Cloud last updated this comment at |
0a12961
to
abc572c
Compare
abc572c
to
0b1e1dd
Compare
) { | ||
const buildAuthorizeRequestµ = buildAuthorizeOptionsµ(wellknown, config, options).pipe( | ||
Micro.flatMap(([url, config, options]) => createAuthorizeUrlµ(url, config, options)), | ||
(effect) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any specific reason we are using this arrow function style here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
some initial looks over will look again
const buildAuthorizeRequestµ = buildAuthorizeOptionsµ(wellknown, config, options).pipe( | ||
Micro.flatMap(([url, config, options]) => createAuthorizeUrlµ(url, config, options)), | ||
(effect) => { | ||
return Micro.matchEffect(effect, { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i'd be curious if just using tapError
and tap
would be better fits here
https://effect-ts.github.io/effect/effect/Micro.ts.html#taperror
https://effect-ts.github.io/effect/effect/Micro.ts.html#tap
tapError would only run on error channels, and tap on success channels
return Micro.gen(function* () { | ||
if ('authorizeResponse' in response) { | ||
log.debug('Received authorize response', response.authorizeResponse); | ||
return yield* Micro.succeed(response.authorizeResponse as AuthorizeSuccessResponse); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the succeed
here is redundant in a gen
it should implicitly return a Micro
here
} | ||
log.error('Error in authorize response', response); | ||
const errorResponse = response as { error: string; error_description: string }; | ||
return yield* createAuthorizeErrorµ(errorResponse, wellknown, config, options); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would we want to Fail
out of this effect here? This keeps the error
in the success channel, is that what we want?
Micro/Effect should always keep Errors are values, so i think it would make sense to Fail
out of it.
* redirect based server supporting iframes. An example would be PingAM. | ||
*/ | ||
return authorizeIframeµ(url, config).pipe( | ||
Micro.flatMap((response) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think given the below changes this could just be a map
now?
|
||
export function authorizeFetchµ(url: string) { | ||
return Micro.tryPromise({ | ||
try: async () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nothing inherently wrong with this, just going to say how I usually write this.
Because tryPromise
takes a Promise, I find it easier to just pass in fetch
with no awaiting. then break down the steps following it in their own sequences. This allows for more granular error handling in my opinion.
Micro.tryPromise({
try: (signal) => fetch('....', { signal }),
catch: (e) => new MyFetchError()
}).pipe(
Micro.flatMap(res => Micro.tryPromise({
try: () => res.json(),
catch: (err) => new ResJsonErr()
})));
(not sure we can use Data if we use Micro, but it's just a helper around making errors, not required in the below example)
https://effect.website/play#5b10175aea48
} | ||
|
||
return { | ||
error: 'Authorization Notwork Failure', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Notwork
spelling?
|
||
export function authorizeIframeµ(url: string, config: OidcConfig) { | ||
return Micro.tryPromise({ | ||
try: async () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
np; unneccessary body?
return err; | ||
} | ||
|
||
const result = await authorizeµ(wellknown, config, log, options); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm curious if it may be more clear to runPromiseExit
here where we explicitly want to run the effect.
this keeps the actual authorize effect self-contained and composable later on?
export async function authorizeµ( | ||
wellknown: WellKnownResponse, | ||
config: OidcConfig, | ||
log: CustomLogger, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this would be a good use case for a service (like we discussed) so we don't need to pass around the logger throughout the api
*/ | ||
return authorizeFetchµ(url).pipe( | ||
Micro.flatMap((response) => { | ||
return Micro.gen(function* () { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I actually think this entire gen
is unneccessary since you're just returning effects you can do that out of flatMap and no need for gen
import type { OidcConfig } from './config.types.js'; | ||
import { AuthorizeSuccessResponse } from './authorize.request.types.js'; | ||
|
||
export async function authorizeµ( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
export async function authorizeµ(
wellknown: WellKnownResponse,
config: OidcConfig,
log: CustomLogger,
options?: GetAuthorizationUrlOptions,
) {
return buildAuthorizeOptionsµ(wellknown, config, options).pipe(
Micro.flatMap(([url, config, options]) => createAuthorizeUrlµ(url, config, options)),
Micro.tap((url) => log.debug('Created authorization URL', { url })),
/**
* This probably needs to change because `log` doesn't reutrn an effect / micro and we want to return one from here
* really `log` needs to be wrapped in a service i guess
*/
Micro.tapError((url) => Micro.fail(log.error('Created authorization URL', { url }))),
Micro.flatMap(([url, config, options]) => {
if (options.responseMode === 'pi.flow') {
/**
* If we support the pi.flow field, this means we are using a PingOne server.
* PingOne servers do not support redirection through iframes because they
* set iframe's to DENY.
*/
return authorizeFetchµ(url).pipe(
Micro.flatMap((response) => {
if ('authorizeResponse' in response) {
log.debug('Received authorize response', response.authorizeResponse);
return Micro.succeed(response.authorizeResponse as AuthorizeSuccessResponse);
}
log.error('Error in authorize response', response);
const errorResponse = response as { error: string; error_description: string };
return Micro.fail(createAuthorizeErrorµ(errorResponse, wellknown, config, options));
}),
);
} else {
/**
* If the response mode is not pi.flow, then we are likely using a traditional
* redirect based server supporting iframes. An example would be PingAM.
*/
return authorizeIframeµ(url, config).pipe(
Micro.flatMap((response) => {
if ('code' in response && 'state' in response) {
log.debug('Received authorization code', response);
return Micro.succeed(response as unknown as AuthorizeSuccessResponse);
}
log.error('Error in authorize response', response);
const errorResponse = response as { error: string; error_description: string };
return Micro.fail(createAuthorizeErrorµ(errorResponse, wellknown, config, options));
}),
);
}
}),
/**
* This could be moved out of this authorize function
*/
Micro.runPromiseExit,
);
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
did a rewrite, feel free to pick and choose but really the key parts are undid a few unneccessary layers of Micro
and then explicitly Fail
when we should return an error.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
one smaller re-write
export function authorizeµ(
wellknown: WellKnownResponse,
config: OidcConfig,
log: CustomLogger,
options?: GetAuthorizationUrlOptions,
) {
return buildAuthorizeOptionsµ(wellknown, config, options).pipe(
Micro.flatMap(([url, config, options]) => createAuthorizeUrlµ(url, config, options)),
Micro.tap((url) => log.debug('Created authorization URL', { url })),
Micro.tapError((url) => Micro.sync(() => log.error('Created authorization URL', { url }))),
Micro.flatMap(([url, config, options]) => {
if (options.responseMode === 'pi.flow') {
return authorizeFetchµ(url).pipe(
Micro.map((response) => {
if ('authorizeResponse' in response) {
return response.authorizeResponse as AuthorizeSuccessResponse;
}
const errorResponse = response as { error: string; error_description: string };
return Micro.fail(createAuthorizeErrorµ(errorResponse, wellknown, config, options));
}),
Micro.tap((response) => log.debug('Received authorize response', response)),
Micro.tapError((response) =>
Micro.sync(() => log.error('Error in authorize response', response)),
),
);
} else {
return authorizeIframeµ(url, config).pipe(
Micro.map((response) => {
if ('code' in response && 'state' in response) {
return response as unknown as AuthorizeSuccessResponse;
}
const errorResponse = response as { error: string; error_description: string };
return Micro.fail(createAuthorizeErrorµ(errorResponse, wellknown, config, options));
}),
Micro.tap((response) => log.debug('Received authorize response', response)),
Micro.tapError((response) =>
Micro.sync(() => log.error('Error in authorize response', response)),
),
);
}
}),
);
}
}), | ||
); | ||
|
||
return Micro.runPromiseExit(buildAuthorizeRequestµ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thinking about this more, wouldn't this be against our pattern of running effects at the edge? i'd think we'd want this to be as close to the edge of the program as possible.
I'd advocate this happen at the top level if possible
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I agree with all of your comments. This one especially. I was actually going to move this to the top file, but didn't get around to it.
const authResponse = oidcClient.authorize.background(); // Returns code and state if successful, error and Auth URL if not | ||
const authUrl = oidcClient.authorize.url(); // Returns Auth URL or error | ||
|
||
// Tokens API |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do the tokens api and user api exist on the OIDC client yet? Didn't see them on the client store
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, this is just the authorize
feature. Token exchange is found here: #348. The other token related methods are not implemeneted yet.
@@ -116,7 +116,7 @@ export default function iFrameManager() { | |||
// 1. Check for Error Parameters | |||
if (hasErrorParams(searchParams, errorParams)) { | |||
cleanup(); | |||
reject(parsedParams); // Reject with all parsed params for context | |||
resolve(parsedParams); // Reject with all parsed params for context |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not reject and catch it with a Micro.fail
in authorizeIframeµ
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is reasonable now that the code is more mature. I'll reevaluate this today.
@@ -0,0 +1,81 @@ | |||
import { CustomLogger } from '@forgerock/sdk-logger'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing copyright in some files
Suggestions for using stronger design patterns for authorize. Treat this as non-working pseudocode.