8000 fix(crux-ui): kratos oidc missing info by robot9706 · Pull Request #773 · dyrector-io/dyrectorio · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

fix(crux-ui): kratos oidc missing info #773

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

Merged
merged 2 commits into from
Aug 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions web/crux-ui/src/const.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export const SERVICE_STATUS_CHECK_INTERVAL = 5000 // sec in millis

export const INVITE_LINK_EXPIRATION = '12h'
export const ATTRIB_CSRF = 'csrf_token'
export const ATTRIB_OIDC_PROVIDER = 'provider'
export const HEADER_SET_COOKIE = 'set-cookie'
export const HEADER_LOCATION = 'location'
export const AUTH_RESEND_DELAY = 30 // seconds
Expand Down
3 changes: 3 additions & 0 deletions web/crux-ui/src/models/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ export type RegisterWithPassword = RegisterBase & {
export type RegisterWithOidc = RegisterBase & {
method: 'oidc'
provider: OidcProvider
email?: string
firstName?: string
lastName?: string
}

export type Register = RegisterWithPassword | RegisterWithOidc
Expand Down
9 changes: 9 additions & 0 deletions web/crux-ui/src/pages/api/auth/register.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,15 @@ const dtoToKratosBody = (dto: Register): UpdateRegistrationFlowBody => {
method: 'oidc',
provider: dto.provider,
csrf_token: dto.csrfToken,
traits:
dto.firstName || dto.lastName
? {
name: {
first: dto.firstName,
last: dto.lastName,
},
}
: null,
}

return body
Expand Down
199 changes: 199 additions & 0 deletions web/crux-ui/src/pages/auth/register-oidc.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
import { SingleFormLayout } from '@app/components/layout'
import { ATTRIB_CSRF, ATTRIB_OIDC_PROVIDER, HEADER_LOCATION } from '@app/const'
import DyoButton from '@app/elements/dyo-button'
import { DyoCard } from '@app/elements/dyo-card'
import DyoForm from '@app/elements/dyo-form'
import { DyoInput } from '@app/elements/dyo-input'
import DyoMessage from '@app/elements/dyo-message'
import DyoSingleFormHeading from '@app/elements/dyo-single-form-heading'
import DyoSingleFormLogo from '@app/elements/dyo-single-form-logo'
import useDyoFormik from '@app/hooks/use-dyo-formik'
import { DyoErrorDto, Register } from '@app/models'
import { API_AUTH_REGISTER, ROUTE_LOGIN, ROUTE_LOGOUT, ROUTE_SETTINGS } from '@app/routes'
import { findAttributes, findError, findMessage, isDyoError, redirectTo, upsertDyoError } from '@app/utils'
import { registerSchema } from '@app/validations'
import { RegistrationFlow } from '@ory/kratos-client'
import { captchaDisabled } from '@server/captcha'
import { cookieOf, forwardCookie } from '@server/cookie'
import kratos, { obtainSessionFromRequest } from '@server/kratos'
import { NextPageContext } from 'next'
import useTranslation from 'next-translate/useTranslation'
import { useRouter } from 'next/dist/client/router'
import Link from 'next/link'
import { useRef, useState } from 'react'
import ReCAPTCHA from 'react-google-recaptcha'
import toast from 'react-hot-toast'

interface RegisterPageProps {
flow: RegistrationFlow
recaptchaSiteKey?: string
}

const RegisterOidcPage = (props: RegisterPageProps) => {
const { t } = useTranslation('register')
const router = useRouter()

const { flow, recaptchaSiteKey } = props

const [ui, setUi] = useState(flow.ui)
const [errors, setErrors] = useState<DyoErrorDto[]>([])

const recaptcha = useRef<ReCAPTCHA>()

const formik = useDyoFormik({
initialValues: {
email: (findAttributes(ui, 'traits.email')?.value as string) ?? '',
firstName: (findAttributes(ui, 'traits.name.first')?.value as string) ?? '',
lastName: (findAttributes(ui, 'traits.name.last')?.value as string) ?? '',
},
validationSchema: registerSchema,
onSubmit: async values => {
const captcha = await recaptcha.current?.executeAsync()

const data: Register = {
method: 'oidc',
flow: flow.id,
csrfToken: findAttributes(ui, ATTRIB_CSRF)?.value,
captcha,
provider: findAttributes(ui, ATTRIB_OIDC_PROVIDER)?.value,
email: values.email,
firstName: values.firstName,
lastName: values.lastName,
}

const res = await fetch(API_AUTH_REGISTER, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
})

if (res.ok) {
window.location.href = res.headers.get(HEADER_LOCATION)
} else if (res.status === 410) {
await router.reload()
} else {
recaptcha.current?.reset()

const result = await res.json()

if (isDyoError(result)) {
setErrors(upsertDyoError(errors, result as DyoErrorDto))
} else if (result?.ui) {
setUi(result.ui)
} else {
toast(t('errors:internalError'))
}
}
},
})

return (
<SingleFormLayout title={t('common:signUp')}>
<DyoSingleFormLogo />

<DyoCard className="p-6 mt-2">
<DyoForm className="flex flex-col" >
<DyoSingleFormHeading>{t('common:signUp')}</DyoSingleFormHeading>

<div className="grid grid-cols-1 md:grid-cols-2 gap-x-4">
<DyoInput
containerClassName="col-span-2"
grow
label={t('common:email')}
name="email"
type="email"
disabled
value={formik.values.email}
message={formik.errors.email ?? findMessage(ui, 'traits.email')}
/>

<DyoInput
label={t('common:firstName')}
name="firstName"
>
value={formik.values.firstName}
message={formik.errors.firstName}
messageType="error"
/>

<DyoInput
label={t('common:lastName')}
name="lastName"
>
value={formik.values.lastName}
message={formik.errors.lastName}
messageType="error"
/>
</div>

{ui.messages?.map((it, index) => (
<DyoMessage className="text-xs italic max-w-xl mt-2" key={`error-${index}`} message={it.text} />
))}

<DyoMessage
className="text-xs italic max-w-xl mt-2"
message={findError(errors, 'captcha', it =>
t(`errors:${it.error}`, {
name: it.value,
}),
)}
messageType="error"
/>

<DyoButton className="px-8 mx-auto mt-8" type="submit">
{t('continue')}
</DyoButton>

<p className="text-bright text-center self-center max-w-xl mt-8">
{t(`whenYouRegister`)}
<a className="font-bold" href="https://dyrectorio.com/privacy" target="_blank" rel="noreferrer">
{t('privacyPolicy')}
</a>
.
</p>

{recaptchaSiteKey ? <ReCAPTCHA ref={recaptcha} size="invisible" sitekey={recaptchaSiteKey} /> : null}
</DyoForm>
</DyoCard>

<div className="flex justify-center text-bright mt-8">
<Link className="font-bold underline" href={ROUTE_LOGOUT}>
{t('common:logOut')}
</Link>
</div>
</SingleFormLayout>
)
}

export default RegisterOidcPage

const getPageServerSideProps = async (context: NextPageContext) => {
const flowId = context.query.flow as string

const session = await obtainSessionFromRequest(context.req)
if (session) {
return redirectTo(ROUTE_SETTINGS)
}

if (!flowId) {
return redirectTo(ROUTE_LOGIN)
}

const flow = await kratos.getRegistrationFlow({
id: flowId,
cookie: cookieOf(context.req),
})

forwardCookie(context, flow)

return {
props: {
flow: flow.data,
recaptchaSiteKey: captchaDisabled() ? null : process.env.RECAPTCHA_SITE_KEY,
},
}
}

export const getServerSideProps = getPageServerSideProps
8 changes: 6 additions & 2 deletions web/crux-ui/src/pages/auth/register.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import DyoSingleFormHeading from '@app/elements/dyo-single-form-heading'
import DyoSingleFormLogo from '@app/elements/dyo-single-form-logo'
import useDyoFormik from '@app/hooks/use-dyo-formik'
import { DyoErrorDto, oidcEnabled, OidcProvider, Register } from '@app/models'
import { API_AUTH_REGISTER, ROUTE_LOGIN, ROUTE_SETTINGS, verificationUrl } from '@app/routes'
import { API_AUTH_REGISTER, registerOidcUrl, ROUTE_LOGIN, ROUTE_SETTINGS, verificationUrl } from '@app/routes'
import {
findAttributes,
findError,
Expand All @@ -27,7 +27,7 @@ import { registerSchema } from '@app/validations'
import { RegistrationFlow } from '@ory/kratos-client'
import { captchaDisabled } from '@server/captcha'
import { cookieOf, forwardCookie } from '@server/cookie'
import kratos, { obtainSessionFromRequest } from '@server/kratos'
import kratos, { obtainSessionFromRequest, registrationOidcInvalid } from '@server/kratos'
import { NextPageContext } from 'next'
import useTranslation from 'next-translate/useTranslation'
import { useRouter } from 'next/dist/client/router'
Expand Down Expand Up @@ -286,6 +286,10 @@ const getPageServerSideProps = async (context: NextPageContext) => {
})
: await kratos.createBrowserRegistrationFlow()

if (flowId && registrationOidcInvalid(flow.data)) {
return redirectTo(registerOidcUrl(flowId))
}

forwardCookie(context, flow)

return {
Expand Down
3 changes: 3 additions & 0 deletions web/crux-ui/src/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export const ROUTE_NEW_PASSWORD = '/auth/settings/new-password'
export const ROUTE_LOGIN = '/auth/login'
export const ROUTE_LOGOUT = '/auth/logout'
export const ROUTE_REGISTER = '/auth/register'
export const ROUTE_REGISTER_OIDC = '/auth/register-oidc'

export const ROUTE_SETTINGS = '/auth/settings'
export const ROUTE_SETTINGS_TOKENS = '/auth/settings/tokens'
Expand Down Expand Up @@ -160,6 +161,8 @@ class DashboardRoutes {
index = () => this.root
}

export const registerOidcUrl = (flow: string) => `${ROUTE_REGISTER_OIDC}?flow=${encodeURIComponent(flow)}`

// audit
class AuditRoutes {
private readonly root: string
Expand Down
4 changes: 4 additions & 0 deletions web/crux-ui/src/server/kratos.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
Identity,
IdentityApi,
MetadataApi,
RegistrationFlow,
Session,
VerifiableIdentityAddress,
} from '@ory/kratos-client'
Expand Down Expand Up @@ -163,4 +164,7 @@ export type IncomingMessageWithSession = http.IncomingMessage & {
export const assambleKratosRecoveryUrl = (flow: string, code: string): string =>
`${process.env.KRATOS_URL}/self-service/recovery?flow=${flow}&code=${code}`

export const registrationOidcInvalid = (flow: RegistrationFlow) =>
flow.ui.nodes.some(it => it.group === 'oidc' && it.messages.length > 0)

export default kratos
4 changes: 2 additions & 2 deletions web/kratos/oidc/azure.schema.jsonnet
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ local claims = std.extVar('claims');
traits: {
[if 'email' in claims then 'email' else null]: claims.email,
name: {
first: claims.given_name,
last: claims.family_name,
first: if 'given_name' in claims then claims.given_name else null,
last: if 'family_name' in claims then claims.family_name else null,
},
},
},
Expand Down
4 changes: 2 additions & 2 deletions web/kratos/oidc/google.schema.jsonnet
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ local claims = {
traits: {
[if 'email' in claims && claims.email_verified then 'email' else null]: claims.email,
name: {
first: claims.given_name,
last: claims.family_name,
first: if 'given_name' in claims then claims.given_name else null,
last: if 'family_name' in claims then claims.family_name else null,
},
},
},
Expand Down
0