-
-
Notifications
You must be signed in to change notification settings - Fork 673
Add support for SAML/etc auth #3675
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,7 +4,12 @@ import React, { PureComponent } from 'react'; | |
| import { Linking } from 'react-native'; | ||
| import type { NavigationScreenProp } from 'react-navigation'; | ||
|
|
||
| import type { AuthenticationMethods, Dispatch, ApiResponseServerSettings } from '../types'; | ||
| import type { | ||
| AuthenticationMethods, | ||
| Dispatch, | ||
| ExternalAuthenticationMethod, | ||
| ApiResponseServerSettings, | ||
| } from '../types'; | ||
| import { IconPrivate, IconGoogle, IconGitHub, IconWindows, IconTerminal } from '../common/Icons'; | ||
| import type { IconType } from '../common/Icons'; | ||
| import { connect } from '../react-redux'; | ||
|
|
@@ -34,7 +39,8 @@ type AuthenticationMethodDetails = {| | |
| action: 'dev' | 'password' | {| url: string |}, | ||
| |}; | ||
|
|
||
| const authentications: AuthenticationMethodDetails[] = [ | ||
| // Methods that don't show up in external_authentication_methods. | ||
| const availableDirectMethods: AuthenticationMethodDetails[] = [ | ||
| { | ||
| name: 'dev', | ||
| displayName: 'dev account', | ||
|
|
@@ -53,6 +59,19 @@ const authentications: AuthenticationMethodDetails[] = [ | |
| Icon: IconPrivate, | ||
| action: 'password', | ||
| }, | ||
| { | ||
| // This one might move to external_authentication_methods in the future. | ||
| name: 'remoteuser', | ||
| displayName: 'SSO', | ||
| Icon: IconPrivate, | ||
| action: { url: 'accounts/login/sso/' }, | ||
| }, | ||
| ]; | ||
|
|
||
| // Methods that are covered in external_authentication_methods by servers | ||
| // which have that key (Zulip Server v2.1+). We refer to this array for | ||
| // servers that don't. | ||
| const availableExternalMethods: AuthenticationMethodDetails[] = [ | ||
| { | ||
| name: 'google', | ||
| displayName: 'Google', | ||
|
|
@@ -74,20 +93,22 @@ const authentications: AuthenticationMethodDetails[] = [ | |
| Icon: IconWindows, | ||
| action: { url: '/accounts/login/social/azuread-oauth2' }, | ||
| }, | ||
| { | ||
| name: 'remoteuser', | ||
| displayName: 'SSO', | ||
| Icon: IconPrivate, | ||
| action: { url: 'accounts/login/sso/' }, | ||
| }, | ||
| ]; | ||
|
|
||
| const externalMethodIcons = new Map([ | ||
| ['google', IconGoogle], | ||
| ['github', IconGitHub], | ||
| ['azuread', IconWindows], | ||
| ]); | ||
|
|
||
| /** Exported for tests only. */ | ||
| export const activeAuthentications = ( | ||
| authenticationMethods: AuthenticationMethods, | ||
| externalAuthenticationMethods: ExternalAuthenticationMethod[] | void, | ||
| ): AuthenticationMethodDetails[] => { | ||
| const result = []; | ||
| authentications.forEach(auth => { | ||
|
|
||
| availableDirectMethods.forEach(auth => { | ||
| if (!authenticationMethods[auth.name]) { | ||
| return; | ||
| } | ||
|
|
@@ -98,6 +119,37 @@ export const activeAuthentications = ( | |
| } | ||
| result.push(auth); | ||
| }); | ||
|
|
||
| if (!externalAuthenticationMethods) { | ||
| // Server doesn't speak new API; get these methods from the old one. | ||
| availableExternalMethods.forEach(auth => { | ||
| if (authenticationMethods[auth.name]) { | ||
| result.push(auth); | ||
| } | ||
| }); | ||
| } else { | ||
| // We have info from new API; ignore old one for these methods. | ||
| externalAuthenticationMethods.forEach(method => { | ||
| if (result.some(({ name }) => name === method.name)) { | ||
| // Ignore duplicate. | ||
| return; | ||
| } | ||
|
|
||
| // The server provides icons as image URLs; but we have our own built | ||
| // in, which we don't have to load and can color to match the button. | ||
| // TODO perhaps switch to server's, for the sake of SAML where ours is | ||
| // generic and the server may have a more specific one. | ||
| const Icon = externalMethodIcons.get(method.name) ?? IconPrivate; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This seems awkward: the server doesn't actually have to send any particular (I have no constructive suggestions here, unfortunately.)
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is actually exactly the sort of thing that
The display name could easily get tweaked in the future, and the login URL certainly has been known to change (see comment about Google auth, above in this file). The |
||
|
|
||
| result.push({ | ||
| name: method.name, | ||
| displayName: method.display_name, | ||
| Icon, | ||
| action: { url: method.login_url }, | ||
| }); | ||
| }); | ||
| } | ||
|
|
||
| return result; | ||
| }; | ||
|
|
||
|
|
@@ -130,8 +182,10 @@ class AuthScreen extends PureComponent<Props> { | |
| } | ||
| }); | ||
|
|
||
| const { serverSettings } = this.props.navigation.state.params; | ||
| const authList = activeAuthentications( | ||
| this.props.navigation.state.params.serverSettings.authentication_methods, | ||
| serverSettings.authentication_methods, | ||
| serverSettings.external_authentication_methods, | ||
| ); | ||
| if (authList.length === 1) { | ||
| this.handleAuth(authList[0]); | ||
|
|
@@ -187,7 +241,10 @@ class AuthScreen extends PureComponent<Props> { | |
| name={serverSettings.realm_name} | ||
| iconUrl={getFullUrl(serverSettings.realm_icon, this.props.realm)} | ||
| /> | ||
| {activeAuthentications(serverSettings.authentication_methods).map(auth => ( | ||
| {activeAuthentications( | ||
| serverSettings.authentication_methods, | ||
| serverSettings.external_authentication_methods, | ||
| ).map(auth => ( | ||
| <ZulipButton | ||
| key={auth.name} | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
(... although on second thought, do we have any use for
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm -- we probably don't! I'll think about that a bit. If we do keep
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, I don't see a point in having On top of which, there's no way for the list of them to change in an update anyway. Looks like the
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
As I type this bit into a commit message, I realize it isn't quite true -- if the user has just touched a button so e.g. the Material "ink splash" animation is in progress, that animation state belongs to the button and not to an unrelated button that might appear in the same spot. ... And although it is true that there's no way for the list to change in an update (it comes from a nav param, so it's part of the historical fact of how we navigated here), that feels like a bit of a fragile fact. So now I'm inclined to say that it's good practice to have a I'll just add the check that the name is unique, then. |
||
| style={styles.halfMarginTop} | ||
|
|
||
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 information already exists as a slice of the
availableExternalMethodstable; perhaps it should be extracted from there?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.
Could do. As a quick experiment just now, I wrote that and it looks like this:
Thinking a bit more, though, I think I prefer the version that recites it explicitly. It's true right now that this has the same values as that slice of
availableExternalMethods, but:availableExternalMethodsis the legacy API, and in general I prefer the implementation of a current API to stand alone without depending on the implementation of a legacy API.availableExternalMethodswon't grow.Of these only the first is a particularly overriding reason. So if this were more complicated I probably would want to share it, and would just do so in the other direction: arrange for
availableExternalMethods(or its use site) to get the data from here, and say YAGNI to the "more in future" point.(Well, we probably are going to need more in future; what that really means is more like "we'll cross that bridge when we come to it, and it'll be fine.")
But this is so simple, it doesn't seem worth complicating the code.