-
Notifications
You must be signed in to change notification settings - Fork 13
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
#195 SSO and SLO additions #236
Conversation
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 questions here and there, overall it makes sense.
I'm lacking instruction on how to test this, and confirm if it runs/works
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 is amazing. I'm thrilled to see SAML2 based SSO logins working for Starchart as part of 0.3! Really great work on this.
I'd like to see us improve this code a bunch, but I don't want to block it landing in 0.3 either. Can you please take a look at my feedback and decide how much you want to do before 0.3, and what you want to do in 0.4. Anything that will happen later should get filed as follow-up issues.
Also, this code is easily testable via Playwright, and I think that should be your primary task in 0.4, so we can always know that it works going forward. See my existing login/logout Playwright test code in Telescope for some ideas:
I'm sure @Eakam1007 can provide guidance too.
.env.example
Outdated
@@ -18,3 +18,7 @@ LOG_LEVEL=debug | |||
# Nodemailer config | |||
NOTIFICATIONS_EMAIL_USER="no-reply@senecacollege.ca" | |||
MAILHOG_SMTP_PORT=1025 | |||
SAML_IDP_METADATA=http://localhost:8081/simplesaml/saml2/idp/metadata.php |
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.
Because Samlify can configure the IdP via its metadata, we should probably make this a path to a file that we load (i.e., readFileSync()
) vs. a URL that we have to download async at startup:
const idp = new IdentityProvider({
// required
metadata: readFileSync('./config/idp-metadata.xml'),
});
The metadata files we need are:
- Dev: http://localhost:8081/simplesaml/saml2/idp/metadata.php
- Staging: https://login.microsoftonline.com/eb34f74a-58e7-4a8b-9e59-433e4c412757/federationmetadata/2007-06/federationmetadata.xml?appid=9b6e9159-c5ab-462b-8efa-3ecb46e8b6df
Also, let's separate this block out to its own section, and add a # SAML Config
comment.
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 a bit more. It might be better to have all of the exports from the saml.server.ts
module be async
. This would allow us to pull the IdP metadata on startup and use that (i.e., first request would get the metadata and config the idp, subsequent runs would be faster since we already have it).
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.
@humphd
I am trying the following but it is claiming that neither http://localhost:8081/simplesaml/saml2/idp/metadata.php
or http://host.docker.internal:8081/simplesaml/saml2/idp/metadata.php
exist
//Take the metadata stood up by the IDP and use it as the metadata for our IDP object
const idp = samlify.IdentityProvider({
metadata: readFileSync(`${process.env.SAML_IDP_METADATA}`),
});
What am I doing wrong?
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.
Wait, I think I misunderstood, you would like to have the idp metadata stored as a file local to starchart, is that correct?
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.
In the last commit I had it read from the idp config locally, when I try to load idp once in saml server at the top level it gives me the error that await can not be used at the top level.
.env.example
Outdated
@@ -18,3 +18,7 @@ LOG_LEVEL=debug | |||
# Nodemailer config | |||
NOTIFICATIONS_EMAIL_USER="no-reply@senecacollege.ca" | |||
MAILHOG_SMTP_PORT=1025 | |||
SAML_IDP_METADATA=http://localhost:8081/simplesaml/saml2/idp/metadata.php | |||
HOSTNAME = http://localhost` |
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 does this not include the port?
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.
Also, trailing backtick
app/routes/login.tsx
Outdated
import { Form } from '@remix-run/react'; | ||
|
||
import { createUserSession } from '~/session.server'; | ||
import { getUsername } from '~/session.server'; | ||
import { getIdp, sp } from '~/saml.server'; |
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.
Let's not expose these, since the rest of our app doesn't need to know about Samlify. Instead, let's expose some higher-level API calls we can make. What about:
createLoginRequest()
and have it deal with idp + sp +'redirect'
logic internally, and return the URL we needcreateLogoutRequest()
same idea
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 added some functions in the saml server so we dont have to export sp and idp but export those instead. Is that sufficient or should there be a seperate file that is the only one to ever import saml.server?
app/routes/login.tsx
Outdated
const idp = await getIdp(); | ||
const { context } = sp.createLoginRequest(idp, 'redirect'); | ||
return redirect(context); | ||
} else { |
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.
No else
after return
. If eslint didn't flag this, we should add it to the rules.
app/routes/login/callback.tsx
Outdated
}); | ||
|
||
//Try and extract the username and see if there is an existing user by that name | ||
if (extract.nameID) { |
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.
According to what I shared with you from ITS, our metadata will look like this:
- displayname
- sAMAccountName (is the username)
- group (currently the values would be: mycustomdomain-dev-admins, mycustomdomain-dev-faculty, mycustomdomain-dev-students)
So we better extract the sAMAccountName
.
See https://login.microsoftonline.com/eb34f74a-58e7-4a8b-9e59-433e4c412757/federationmetadata/2007-06/federationmetadata.xml?appid=9b6e9159-c5ab-462b-8efa-3ecb46e8b6df for the metadata, and in particular, notice the claims it defines:
<fed:ClaimTypesOffered>
<auth:ClaimType
xmlns:auth="http://docs.oasis-open.org/wsfed/authorization/200706"
Uri="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name"
>
<auth:DisplayName>Name</auth:DisplayName>
<auth:Description>The mutable display name of the user.</auth:Description>
</auth:ClaimType>
<auth:ClaimType
xmlns:auth="http://docs.oasis-open.org/wsfed/authorization/200706"
Uri="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier"
>
<auth:DisplayName>Subject</auth:DisplayName>
<auth:Description>
An immutable, globally unique, non-reusable identifier of the user that is
unique to the application for which a token is issued.
</auth:Description>
</auth:ClaimType>
<auth:ClaimType
xmlns:auth="http://docs.oasis-open.org/wsfed/authorization/200706"
Uri="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname"
>
<auth:DisplayName>Given Name</auth:DisplayName>
<auth:Description>First name of the user.</auth:Description>
</auth:ClaimType>
<auth:ClaimType
xmlns:auth="http://docs.oasis-open.org/wsfed/authorization/200706"
Uri="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname"
>
<auth:DisplayName>Surname</auth:DisplayName>
<auth:Description>Last name of the user.</auth:Description>
</auth:ClaimType>
<auth:ClaimType
xmlns:auth="http://docs.oasis-open.org/wsfed/authorization/200706"
Uri="http://schemas.microsoft.com/identity/claims/displayname"
>
<auth:DisplayName>Display Name</auth:DisplayName>
<auth:Description>Display name of the user.</auth:Description>
</auth:ClaimType>
<auth:ClaimType
xmlns:auth="http://docs.oasis-open.org/wsfed/authorization/200706"
Uri="http://schemas.microsoft.com/identity/claims/nickname"
>
<auth:DisplayName>Nick Name</auth:DisplayName>
<auth:Description>Nick name of the user.</auth:Description>
</auth:ClaimType>
<auth:ClaimType
xmlns:auth="http://docs.oasis-open.org/wsfed/authorization/200706"
Uri="http://schemas.microsoft.com/ws/2008/06/identity/claims/authenticationinstant"
>
<auth:DisplayName>Authentication Instant</auth:DisplayName>
<auth:Description>
The time (UTC) when the user is authenticated to Windows Azure Active
Directory.
</auth:Description>
</auth:ClaimType>
<auth:ClaimType
xmlns:auth="http://docs.oasis-open.org/wsfed/authorization/200706"
Uri="http://schemas.microsoft.com/ws/2008/06/identity/claims/authenticationmethod"
>
<auth:DisplayName>Authentication Method</auth:DisplayName>
<auth:Description>
The method that Windows Azure Active Directory uses to authenticate users.
</auth:Description>
</auth:ClaimType>
<auth:ClaimType
xmlns:auth="http://docs.oasis-open.org/wsfed/authorization/200706"
Uri="http://schemas.microsoft.com/identity/claims/objectidentifier"
>
<auth:DisplayName>ObjectIdentifier</auth:DisplayName>
<auth:Description>
Primary identifier for the user in the directory. Immutable, globally
unique, non-reusable.
</auth:Description>
</auth:ClaimType>
<auth:ClaimType
xmlns:auth="http://docs.oasis-open.org/wsfed/authorization/200706"
Uri="http://schemas.microsoft.com/identity/claims/tenantid"
>
<auth:DisplayName>TenantId</auth:DisplayName>
<auth:Description>Identifier for the user's tenant.</auth:Description>
</auth:ClaimType>
<auth:ClaimType
xmlns:auth="http://docs.oasis-open.org/wsfed/authorization/200706"
Uri="http://schemas.microsoft.com/identity/claims/identityprovider"
>
<auth:DisplayName>IdentityProvider</auth:DisplayName>
<auth:Description>Identity provider for the user.</auth:Description>
</auth:ClaimType>
<auth:ClaimType
xmlns:auth="http://docs.oasis-open.org/wsfed/authorization/200706"
Uri="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"
>
<auth:DisplayName>Email</auth:DisplayName>
<auth:Description>Email address of the user.</auth:Description>
</auth:ClaimType>
<auth:ClaimType
xmlns:auth="http://docs.oasis-open.org/wsfed/authorization/200706"
Uri="http://schemas.microsoft.com/ws/2008/06/identity/claims/groups"
>
<auth:DisplayName>Groups</auth:DisplayName>
<auth:Description>Groups of the user.</auth:Description>
</auth:ClaimType>
<auth:ClaimType
xmlns:auth="http://docs.oasis-open.org/wsfed/authorization/200706"
Uri="http://schemas.microsoft.com/identity/claims/accesstoken"
>
<auth:DisplayName>External Access Token</auth:DisplayName>
<auth:Description>
Access token issued by external identity provider.
</auth:Description>
</auth:ClaimType>
<auth:ClaimType
xmlns:auth="http://docs.oasis-open.org/wsfed/authorization/200706"
Uri="http://schemas.microsoft.com/ws/2008/06/identity/claims/expiration"
>
<auth:DisplayName>External Access Token Expiration</auth:DisplayName>
<auth:Description>
UTC expiration time of access token issued by external identity provider.
</auth:Description>
</auth:ClaimType>
<auth:ClaimType
xmlns:auth="http://docs.oasis-open.org/wsfed/authorization/200706"
Uri="http://schemas.microsoft.com/identity/claims/openid2_id"
>
<auth:DisplayName>External OpenID 2.0 Identifier</auth:DisplayName>
<auth:Description>
OpenID 2.0 identifier issued by external identity provider.
</auth:Description>
</auth:ClaimType>
<auth:ClaimType
xmlns:auth="http://docs.oasis-open.org/wsfed/authorization/200706"
Uri="http://schemas.microsoft.com/claims/groups.link"
>
<auth:DisplayName>GroupsOverageClaim</auth:DisplayName>
<auth:Description>
Issued when number of user's group claims exceeds return limit.
</auth:Description>
</auth:ClaimType>
<auth:ClaimType
xmlns:auth="http://docs.oasis-open.org/wsfed/authorization/200706"
Uri="http://schemas.microsoft.com/ws/2008/06/identity/claims/role"
>
<auth:DisplayName>Role Claim</auth:DisplayName>
<auth:Description>
Roles that the user or Service Principal is attached to
</auth:Description>
</auth:ClaimType>
<auth:ClaimType
xmlns:auth="http://docs.oasis-open.org/wsfed/authorization/200706"
Uri="http://schemas.microsoft.com/ws/2008/06/identity/claims/wids"
>
<auth:DisplayName>RoleTemplate Id Claim</auth:DisplayName>
<auth:Description>
Role template id of the Built-in Directory Roles that the user is a member
of
</auth:Description>
</auth:ClaimType>
</fed:ClaimTypesOffered>
app/routes/login/callback.tsx
Outdated
} | ||
|
||
// return to access denied if redirect in createUserSession did not take | ||
return redirect('/access_denied'); |
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 page doesn't exist
app/saml.server.ts
Outdated
samlify.setSchemaValidator(validator); | ||
|
||
//Here we configure the service provider: https://samlify.js.org/#/sp-configuration | ||
const spData = { |
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.
Move this object into line 27 (i.e., don't add a variable to pass it to the constructor). The reason for this is type checking. Here, you have an any
type, but if you do it in the argument list of ServiceProvider({...})
it will get typed.
@humphd I have been looking at this a lot over the past day and I don't know if it works exactly as intended. Validation of signature along with what is meant to be signed (right now I am trying to have the assertion signed but upon further inspection don't know if that's working and whether I'm supposed to be signing the assertions or the message), and the logout flow along with parsing the logout response might need to be added or changed. I don't think I can do all of that for this MS, I am going to do some cleaning up of the current code and try to get this in and change it moving forward if that's ok with you. It seems to work but I think I have missed the mark on a lot of important things. |
I agree that nailing down all the various pieces of this is beyond the scope of 0.3. Since what you have works in development, I'm inclined to say that we should fix the code-level issues I've highlighted, and file issues on the rest to do in 0.4+. Let's not block this on being perfect. You've moved us ahead a long way, and that's what a weekly milestone is all about. |
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 don't see how to get this landed in 0.3, since this is pretty big and still needs work. We need PRs to come in sooner so there is time to review in future.
app/routes/login/callback.tsx
Outdated
const body = Object.fromEntries(formData); | ||
const extract = await parseLoginResponse(body); | ||
|
||
//Try and extract the username and see if there is an existing user by that name |
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.
nit: sometimes you do //nospace
and other times // space
. Can you always do the latter (i.e., include a space after //
)?
app/routes/login/callback.tsx
Outdated
const extract = await parseLoginResponse(body); | ||
|
||
//Try and extract the username and see if there is an existing user by that name | ||
if (extract.attributes.sAMAccountName) { |
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.
Same advice as before, flip your logic and put the error/shorter case first:
if(!extract.attribues.sAMAccountName) {
// TODO: Create some form of access denied notification to redirect to here
return redirect('/access_denied');
}
// bulk of code here, not indented...
app/routes/login/callback.tsx
Outdated
export const action = async ({ request }: ActionArgs) => { | ||
const formData = await request.formData(); | ||
|
||
if (request.method != 'POST') { |
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 this first, since a only a POST
will have form data to parse.
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.
Also, !==
vs. !=
in JS please. The former does implicit type casting, which you never want.
app/routes/login/callback.tsx
Outdated
} | ||
|
||
const body = Object.fromEntries(formData); | ||
const extract = await parseLoginResponse(body); |
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.
extract
is an odd name. samlResponse
?
app/routes/logout/callback.tsx
Outdated
|
||
if (SAMLResponse) { | ||
return await logout(request); | ||
} else { |
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.
No else-after-return.
app/saml.server.ts
Outdated
|
||
//Take the metadata stood up by the IDP and use it as the metadata for our IDP object | ||
const idp = samlify.IdentityProvider({ | ||
metadata: readFileSync(`${process.env.SAML_IDP_METADATA}`), |
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.
If it's a filename vs. the contents of a file, you should name it as such: SAML_IDP_METADATA_FILE
or _FILENAME
.
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 needs a few fixes, but I was able to make it work.
You should update CONTRIBUTING.md
to include a section on how to login (i.e., a table with usernames/passwords) so we can point our devs at it. None of them are going to know how to login after you land this!
.env.example
Outdated
@@ -18,3 +18,7 @@ LOG_LEVEL=debug | |||
# Nodemailer config | |||
NOTIFICATIONS_EMAIL_USER="no-reply@senecacollege.ca" | |||
MAILHOG_SMTP_PORT=1025 | |||
SAML_IDP_METADATA_FILE='./config/idp-metadata.xml' |
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.
Put the contents of this file in dev-secrets/IDP_METADATA
vs. here.
app/routes/login.tsx
Outdated
const context = await createLoginRequest(); | ||
return redirect(context); | ||
} | ||
if (user) { |
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 is unnecessary, based on line 25. You can simply return ...
here.
.env.example
Outdated
@@ -18,3 +18,7 @@ LOG_LEVEL=debug | |||
# Nodemailer config | |||
NOTIFICATIONS_EMAIL_USER="no-reply@senecacollege.ca" | |||
MAILHOG_SMTP_PORT=1025 | |||
SAML_IDP_METADATA_FILE='./config/idp-metadata.xml' |
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.
Also, please separate this section with a blank line and a comment:
# SSO Config
...
app/routes/login/callback.tsx
Outdated
|
||
const formData = await request.formData(); | ||
const body = Object.fromEntries(formData); | ||
const samlResponse = await parseLoginResponse(body); |
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.
Because you only use attributes
, you might as well pull that out here:
const { attributes } = await parseLoginResponse(body);
app/routes/login/callback.tsx
Outdated
request: request, | ||
username: username, | ||
remember: false, | ||
redirectTo: '/', |
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 we have access to the redirectTo
param here? When you are on a page, and get redirected to the login, it usually keeps this param so you can get sent back to the place where you were. It should be in params
I think, but check my math.
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 remembered that Chris's example had something for this using RelayState so that the redirectUrl can persist to the callback. I added that.
app/saml.server.ts
Outdated
|
||
// Take the metadata stood up by the IDP and use it as the metadata for our IDP object | ||
const idp = samlify.IdentityProvider({ | ||
metadata: readFileSync(`${process.env.SAML_IDP_METADATA_FILE}`), |
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.
Pull this out of secrets
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.
Looking good, a few small things.
app/routes/login.tsx
Outdated
if (!user) { | ||
const context = await createLoginRequest(); | ||
const url = new URL(request.url); | ||
const returnTo = url.searchParams.get('redirectTo') || '/'; |
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 you are meant to do this (i.e., pass the relayState
) in the SP code, see https://github.com/tngan/samlify/blob/87aa1cf7fc3729282d31be60dd248d4906dc491f/src/entity-idp.ts#L83-L91
app/routes/login/callback.tsx
Outdated
request: request, | ||
username: username, | ||
remember: false, | ||
// redirectTo: '/', |
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.
Remove this
package.json
Outdated
@@ -6,7 +6,7 @@ | |||
"build": "run-s build:*", | |||
"build:remix": "remix build", | |||
"build:server": "esbuild --platform=node --format=cjs ./server.ts --outdir=build --bundle", | |||
"dev": "SECRETS_OVERRIDE=1 run-p dev:*", | |||
"dev": "cross-env SECRETS_OVERRIDE=1 run-p dev:*", |
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.
Rebase to pick this up from main
app/saml.server.ts
Outdated
|
||
// Take the metadata stood up by the IDP and use it as the metadata for our IDP object | ||
const idp = samlify.IdentityProvider({ | ||
metadata: readFileSync(secrets.SAML_IDP_METADATA_FILE), |
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.
Three things:
- You don't need to
readFileSync()
on thesecrets
, they are already re-hydrated from the filesystem - Rename it to
SAML_IDP_METADATA
(drop the_FILE
since they are all files) - You should add some code at the top of this to check that this secret exists, and throw if not. We can't run the app without it.
const { SAML_IDP_METADATA } = secrets;
if(!SAML_IDP_METADATA) {
throw new Error('Missing SAML_IDP_METADATA secret');
}
Not entirely sure but after most recent changes I get the following whenever I try to log in:
I'll take a look again tomorrow |
That stack implies that for some reason the IDP's single sign on config is wrong, see https://github.com/tngan/samlify/blob/18ef1392eb92f9fcaa79d0fafe25e9d8d2fcb1cf/src/binding-redirect.ts#L93
|
I put the path to the xml file in config in the secret, when I should have probably just been putting the xml in the secret directly, that seems to do it. |
Should I delete the metadata file in the config folder? |
app/routes/login.tsx
Outdated
|
||
// If not then create a login request to the IDP's redirect binding | ||
if (!user) { | ||
const samlRedirectURL = await createLoginRequest(new URL(request.url)); |
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.
Question: what is request.url
going to be here?
What about the case of http://localhost:8080/login?redirectTo=%2Fdev
, where I want to go to /dev
but get redirected to the login page?
app/routes/login/callback.tsx
Outdated
|
||
const formData = await request.formData(); | ||
const body = Object.fromEntries(formData); | ||
const { attributes } = await parseLoginResponse(body); |
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.
Can you also destructure RelateState
at the same time there?
app/routes/login/callback.tsx
Outdated
// TODO: Make this redirect to access denied page | ||
return redirect('/'); | ||
} | ||
const returnTo: string = body.RelayState ? body.RelayState.toString() : '/'; |
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.
Don't specify a type if you don't have to (seems odd I know). TS can infer most types. Only decorate with types when it can't.
app/saml.server.ts
Outdated
|
||
export async function createLoginRequest(url?: URL) { | ||
const { context } = sp.createLoginRequest(idp, 'redirect'); | ||
const returnTo = url ? url.searchParams.get('redirectTo') : '/'; |
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.
Nice
should I be casting relayState to string?
the above occurs on 78 in login/callback.tsx |
What does All of the Samlify tests use this pattern: const { samlContent, extract } = await sp.parseLoginResponse(idpcustomNoEncrypt, 'redirect', parseRedirectUrlContextCallBack(context));
t.is(typeof id, 'string');
t.is(samlContent.startsWith('<samlp:Response'), true);
t.is(samlContent.endsWith('/samlp:Response>'), true);
t.is(extract.nameID, 'user@esaml2.com');
t.is(extract.attributes.name, 'mynameinsp');
t.is(extract.attributes.mail, 'myemailassociatedwithsp@sp.com');
t.is(extract.response.inResponseTo, '_4606cc1f427fa981e6ffd653ee8d6972fc5ce398c4'); So is it on |
I created a method that is named the same as the sp one, I take relayState out of the body. I don't think the relayState is returned by the sp method. |
OK. I'm not as deep into the changes you've made on this as you are, so you tell me where this is at and let's get this finished. |
on line 54 of saml.server.ts |
app/routes/login.tsx
Outdated
// If not then create a login request to the IDP's redirect binding | ||
if (!user) { | ||
const url = new URL(request.url); | ||
const redirectTo = url.searchParams.get('redirectTo'); |
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.
const redirectTo = url.searchParams.get('redirectTo') ?? undefined;
This will give us undefined
if we get back null
, which is better for the createLoginRequest
params. I'll comment below on how to do it.
app/saml.server.ts
Outdated
|
||
export async function createLoginRequest(redirectUrl?: string | null) { | ||
const { context } = sp.createLoginRequest(idp, 'redirect'); | ||
const returnTo = redirectUrl ? redirectUrl : '/'; |
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.
redirectUrl || '/'
app/saml.server.ts
Outdated
export async function createLoginRequest(redirectUrl?: string | null) { | ||
const { context } = sp.createLoginRequest(idp, 'redirect'); | ||
const returnTo = redirectUrl ? redirectUrl : '/'; | ||
return context + '&RelayState=' + returnTo; |
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.
A better way to do this is to use the built-in URL
constructor that the browser/node provide us (avoid hand-crafting complex strings like URLs, paths, etc as much as you can):
const url = new URL(context);
url.searchParams.append('RelayState', returnTo);
return url.href;
app/saml.server.ts
Outdated
body, | ||
}); | ||
const relayState = body.RelayState as string; | ||
return { samlResponse: extract, relayState: relayState }; |
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.
Don't repeat when key/value are the same:
return { samlResponse: extract, relayState };
app/saml.server.ts
Outdated
return sp.getMetadata(); | ||
} | ||
|
||
export async function createLoginRequest(redirectUrl?: string | null) { |
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.
You can avoid the ?
and | null
dance here if you always pass string
or undefined
(which my change above will do). Here you can set a default value:
export async function createLoginRequest(redirectUrl: string = '/') {
You also have a conflict with |
…all .env change SSO and SLO with SimpleSAMLPhp and Samlify Added SLO callback
…lServer, new samlServer functions to export funcionality without exposing sp/idp
…me guard clauses, added file to metadata file name
…relayState stuff in the logic inside of the saml server
…ectTo searchParams and if it comes from login directly we just use '/', added taking relaystate out of body in parseLoginResponse and destructure it in callback file.
…r in createLoginRequest
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.
Excellent. Thanks for all your work writing and evolving this.
Thank you for the help, lets get one more review |
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 was able to test it successfully on my machine. Run into a small issue, and @sfrunza13 was able to help me figure out what I was doing wrong.
Great work!
* SAML Server, login callback, login page change, sp metadata route, small .env change SSO and SLO with SimpleSAMLPhp and Samlify Added SLO callback * linting concerns * else * POST check first, sAMAccountName extract, idp and sp only used in samlServer, new samlServer functions to export funcionality without exposing sp/idp * Add idp config locally * cleaned up the code comments, swaped the conditional logic to make some guard clauses, added file to metadata file name * forgot a console log * I think this addresses most things * Metadata secret check and remove fs in saml server, tried to add the relayState stuff in the logic inside of the saml server * Changed the metadata location properly * Changed the loginRequest a bit * passing a string into creating login request so that we can use redirectTo searchParams and if it comes from login directly we just use '/', added taking relaystate out of body in parseLoginResponse and destructure it in callback file. * narrowed down type of relayState to string * I forgot to save the changes last time * changed to string or default '/' for createLogin, used URL constructor in createLoginRequest * addition to env example --------- Co-authored-by: stefanaz2 <sfrunza@seneca.ca>
This Pull Request aims to close #195
I used the previously added IDP and configured Starchart as a service provider that requests login and logout to it using redirect and post bindings. The SP metadata is generated by Samlify methods and then exposed on the /sp route.
The SAML response will have its assertions signed by the IDP.
The "POST-BINDING" for the logout actually is a get request and the response is found in the url.
TO TEST THIS
You can take the code and pull it into a branch of your own or with gh cli or however you choose to do it and run the regular steps to run starchart. The login container should be up and running in docker and the login button will redirect to the IDP and the signout will also do the same. With the container running you can go here http://localhost:8081/simplesaml/saml2/idp/metadata.php?output=xhtml and see the IDP Metadata.
testing account for IDP:
username: user1
password: user1pass
You can also go into the network tab and check the flow of the SAML requests and responses through every HTTP request. The payload will have SAML response or SAML request and you can plug them into a decoder and get the XML of the request or response. In this way you can see that the response for example has signed assertions.