-
-
Notifications
You must be signed in to change notification settings - Fork 5.3k
All: Add SAML support #11865
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: dev
Are you sure you want to change the base?
All: Add SAML support #11865
Conversation
This is due to changes in the lib package caused by adding SAML support. Currently, the CLI does not support SAML auth, this only fixes regular Joplin Server sync.
CLA Assistant Lite bot All contributors have signed the CLA ✍️ ✅ |
I have read the CLA Document and I hereby sign the CLA |
Thanks for creating this pull request! At the moment it has some issues related to the linter, which you should be able to fix by running |
I fixed the issues related to the linter. However, the server image does not build since the XML schema validator ( |
Hmm, if it's just to validate an XML schema I guess it's not worth adding a Java dependency? From their doc it looks like there's a TypeScript package too? |
xsd-schema-validator is replaced with xmllint-wasm, to remove any dependency to another program
I got rid of the Java dependency, and replaced the schema validator with |
Sorry to bump this, but is there any update about this? |
Hello, sorry for the lack of feedback yet. As this is a large pull request I will need more time to review it. The fact that it is deeply integrated to both the server and apps mean there will be maintenance concerns since unlike for example a sync target that works independently, the new code will have to be maintained by us probably over time. I don't assume it's possible to make things a bit more modular? i.e. most of the code in new files, and a few integration points here and there; |
Add comments, and clean up the code a bit
I don't think I can make the code more modular than it currently is. I refactored it a bit to help with your review (mostly by adding comments, but also by doing a bit of cleanup), and merged the current The new SAML sync target extends the base Joplin Server one, so on this topic I think I made things more modular than they were before by allowing the sync target to provide a session to Unfortunately since the SAML login process relies on a web browser, I didn't have a choice but to rely on a callback from a web page which means that I needed to use the And on the server, I had to modify If you have any tips about making this better, feel free to tell me so I can implement them. |
Thank you for the detailed explanation, it does help. I also need to go back to your top post and check the diagram again so I understand things at a higher level before diving into the code review. I will try to do that as soon as possible and will get back to you. |
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.
Thank you for the pull request. It looks good overall, although my main concern is whether it could add new security vulnerabilities, but I didn't notice anything obvious.
Regarding the use of x-callback-url
- this is often not very reliable. I'm not sure it works in all operating systems and most browsers don't render these links for security reasons. Also does it work on mobile?
As an alternative, at least on desktop, have you considered launching a small server from the Joplin app and having the browser call some URL on that server? This is what we do for OneDrive in onedrive-api-node-utils.js
for example.
Another solution could to be have the browser display some code that the user would need to copy and paste into the app? (that's what Dropbox does)
packages/app-desktop/main.js
Outdated
if (process.defaultApp) { | ||
if (process.argv.length >= 2) { | ||
electronApp.setAsDefaultProtocolClient('joplin', process.execPath, [path.resolve(process.argv[1])]); | ||
} | ||
} else { | ||
electronApp.setAsDefaultProtocolClient('joplin'); | ||
} |
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.
We need comments here to explain what this does.
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 a bit concerned about security issues too as I know we had one with xCallbackUrl before
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 originally did this to allows the joplin://
protocol to work properly within my development environment, since otherwise the protocol was not properly registered (it registered the electron
binary, but not the main script needed to start Joplin). In production using just electronApp.setAsDefaultProtocolClient('joplin')
is fine since the executable bundles both Electron and the main script. I took this snippet from the Electron docs: https://www.electronjs.org/docs/latest/tutorial/launch-app-from-url-in-another-app.
Since I removed the dependency on x-callback-url
, I also reverted this, since this is likely out of the scope of this PR now.
if (canOpen) { | ||
await Linking.openURL(link); | ||
} else { | ||
Alert.alert(_('Warning'), _('No web browser is installed on your device.')); |
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.
Is that even possible? And if it, do we need to handle that error?
And the comment above mentions "sso-saml-app" - so isn't it what we want support for?
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.
FYI: As someone who has debugged an issue where the problem was a device without a browser, this is definitely possible, especially with devices that are company owned
@@ -460,6 +485,12 @@ class ConfigScreenComponent extends BaseScreenComponent<ConfigScreenProps, Confi | |||
|
|||
if (settings['sync.target'] === SyncTargetRegistry.nameToId('joplinCloud')) { | |||
addSettingButton('go_to_joplin_cloud_login_button', _('Connect to Joplin Cloud'), this.goToJoplinCloudLogin_); | |||
} else if (settings['sync.target'] === SyncTargetRegistry.nameToId('joplinServerSaml')) { | |||
addSettingButton('login_joplin_server_saml_button', _('Connect using your organization account'), this.goToJoplinServerSamlLogin_); |
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.
addSettingButton('login_joplin_server_saml_button', _('Connect using your organization account'), this.goToJoplinServerSamlLogin_); | |
addSettingButton('login_joplin_server_saml_button', _('Connect using your organisation account'), this.goToJoplinServerSamlLogin_); |
addSettingButton('login_joplin_server_saml_button', _('Connect using your organization account'), this.goToJoplinServerSamlLogin_); | ||
|
||
if (Setting.value('sync.11.id') !== '' || Setting.value('sync.11.user_id') !== '') { | ||
addSettingButton('logout_joplin_server_saml_button', _('Log out of Joplin Server'), this.logoutJoplinServerSaml_); |
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.
addSettingButton('logout_joplin_server_saml_button', _('Log out of Joplin Server'), this.logoutJoplinServerSaml_); | |
addSettingButton('logout_joplin_server_saml_button', _('Logout'), this.logoutJoplinServerSaml_); |
packages/app-mobile/root.tsx
Outdated
@@ -1197,6 +1199,10 @@ class AppComponent extends React.Component { | |||
}); | |||
break; | |||
|
|||
case CallbackUrlCommand.SamlLogin: | |||
saveTokens(params.id, params.user_id); | |||
Alert.alert(_('Synchronization'), _('You are now logged into your organization account.')); |
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.
Alert.alert(_('Synchronization'), _('You are now logged into your organization account.')); | |
Alert.alert(_('Synchronisation'), _('You are now logged into your organisation account.')); |
packages/server/src/utils/auth.ts
Outdated
// If no hash is stored for a given user, that means that this user was created using a SSO solution | ||
// such as a SAML login, and is expected to log in through that. | ||
if (!hash) { | ||
return false; | ||
} | ||
|
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 not a fan of this. Why would this function be used if the user doesn't have a password? Maybe the caller needs to check for this?
packages/server/src/utils/saml.ts
Outdated
// Checks if SAML support is enabled. | ||
// | ||
// Throws an error otherwise. | ||
function checkIfSamlIsEnabled() { |
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.
Please use arrow functions only (see coding style guide). All functions here should be converted to arrow functions
packages/server/src/utils/saml.ts
Outdated
// @param relayState The relay state to use for any subsequent login requests. | ||
// @returns A ServiceProvider object. |
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 jsdoc - applies to all code in this file
packages/server/src/utils/saml.ts
Outdated
<title>${_('Joplin SSO Authentication')}</title> | ||
</head> | ||
<body> | ||
<p>${_('Please wait while we load your organization sign-in page...')}</p> |
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.
We use British English in user facing strings - organisation (in code we use US English)
|
||
<div class="columns"> | ||
<form action="{{{global.baseUrl}}}/login" method="POST" class="column"> | ||
<h2 class="title">Login to {{global.appName}}</h1> | ||
<p class="subtitle">Please input your details to login to {{global.appName}}</p> | ||
|
||
<div class="field"> | ||
<label class="label">Email</label> | ||
<div class="control"> | ||
<input class="input" type="email" name="email"/> | ||
</div> | ||
</div> | ||
<div class="field"> | ||
<label class="label">Password</label> | ||
<div class="control"> | ||
<input class="input" type="password" name="password"/> | ||
</div> | ||
<p class="help"><a href="{{{global.baseUrl}}}/password/forgot">I forgot my password</a></p> |
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 reason why the default login method was changed? I would expect only the SAML block needs to be added?
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 updated my branch with your feedback. I removed the use of This required additional modifications to the server side of things to handle auth codes (mostly On the clients, the removal of the This is tested on desktop (Windows 10) and mobile (both Android and iOS). |
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.
Thanks @ttcchhmm, that looks very good. I left a few comments which hopefully shouldn't be hard to address. If you have any question feel free to let me know!
|
||
if (Object.keys(params).length === 1 && params.id) { // Single argument | ||
void CommandService.instance().execute(command.toString(), params.id); | ||
} else { // Multiple arguments | ||
void CommandService.instance().execute(command.toString(), ...Object.values(params)); | ||
} |
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.
Is you no longer use x-callback-url, do we still need this? (and other x-callback-url related changes)
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}> | ||
<div style={containerStyle}> | ||
<p style={theme.textStyle}>{_('To allow Joplin to synchronise with your account, please follow these steps:')}</p> | ||
<ol> | ||
<li style={listItemStyle}> | ||
<button style={buttonStyle} onClick={props.shared.openLoginPage}>{_('Log in with your web browser')}</button> | ||
</li> | ||
<li style={listItemStyle}> | ||
<div> | ||
<label htmlFor='sso-code' style={theme.textStyle}>{_('Enter the code:')}</label> | ||
<input id='sso-code' type='text' style={inputStyle} value={code} onChange={e => setCode(e.target.value)} placeholder='###-###-###' /> | ||
</div> | ||
</li> | ||
<li style={listItemStyle}> | ||
<button type='submit' onClick={submit} disabled={!props.shared.isLoginCodeValid(code)} style={buttonStyle}>{_('Continue')}</button> | ||
</li> | ||
</ol> | ||
</div> | ||
|
||
<ButtonBar onCancelClick={back} /> | ||
</div> | ||
); |
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 need to be styled using RSCSS. See here:
value: '', | ||
type: SettingItemType.String, | ||
section: 'sync', | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied |
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.
For new code please don't use "any". I believe you can get the type here using BuiltInMetadataKeys
?
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.
The show
function gets the type of its any
parameter from the SettingItem
interface, and if I change that to BuiltInMetadataKeys
I get an error since that creates a circular dependency within the types themselves. This change would also require additional modifications to code that use this function (like the other settings entries in this file, or anything that relies on config-shared.ts
). I don't know what to do here, so maybe you have a clue on where to start?
packages/server/src/utils/types.ts
Outdated
identityProviderConfigFile?: string; | ||
serviceProviderConfigFile?: string; | ||
organizationDisplayName?: string; |
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.
Please make all these properties compulsory: https://joplinapp.org/help/dev/coding_style/#avoid-default-and-optional-parameters
Just set them to an empty string if not in use
Do I have to do something to fix the CLA CI job? I did sign the CLA when I started this pull request, so this should pass. |
This PR adds SAML support to Joplin.
Server
Based on the
samlify
library that provides the SAML logic flow for Joplin Server.This adds the following environment variables used as configuration parameters to Joplin Server :
SAML_ENABLED
: If set totrue
, enables SAML support.DISABLE_BUILTIN_LOGIN_FLOW
: If set totrue
, all auth requests MUST go though SAML. Users can't log-in using Joplin-specific credentials and/or LDAP.SAML_IDP_CONFIG_FILE
: Should be a path to an XML file containing the metadata for the Identity Provider (IDP).SAML_SP_CONFIG_FILE
: Should be a path to an XML file containing the metadata for the Service Provider (SP, in this case Joplin).SAML_ORGANIZATION_DISPLAY_NAME
: Name of the organization, as shown on the log-in screen. Optional.The XML files are standard SAML IDP/SP metadata that should be created by the identity solution.
Clients
As for the clients themselves, no additional libraries are needed, since the actual log-in process is happening in a web browser, outside of Joplin itself.
It also adds a new sync target, based on the one for Joplin Server: "Joplin Server (Beta, SAML)". We kept "Beta" in the name for this since the main Joplin Server target itself is currently considered as such.
Important
The log-in flow uses a callback to a
joplin://
URL, and thus requires that only one instance of Joplin is running at any given time. This is important for the desktop client, since the single instance lock is not enforced in thedev
environment.Log-in flow
The log-in process differs slightly if started from within a client or within the server web interface.
Testing and development
For development purposes, we used
saml-idp
as the Identity Provider, as it allows to quickly create new users on the fly and is simple to set up. After generating the keypair (look at thesaml-idp
documentation to see how), just runningnpx saml-idp --acsUrl 'http://localhost:22300/api/saml' --audience http://localhost:22300 --issuer 'saml-idp'
is enough to get a test Identity Provider running, assuming that Joplin Server is running onlocalhost:22300
.Since
saml-idp
does not support generating SP metadata, here is a sample configuration for the Service Provider part :