feat(oauth): update OAuth UX

pull/2749/head
Anthony Lapenna 2019-02-14 15:58:45 +13:00
parent 16226b1202
commit de76ba4e67
10 changed files with 278 additions and 139 deletions

View File

@ -55,7 +55,7 @@ func (handler *Handler) validateOAuth(w http.ResponseWriter, r *http.Request) *h
}
if user == nil && !settings.OAuthSettings.OAuthAutoCreateUsers {
return &httperror.HandlerError{http.StatusForbidden, "Unregistered account", portainer.ErrUnauthorized}
return &httperror.HandlerError{http.StatusForbidden, "Account not created beforehand in Portainer and automatic user provisioning not enabled", portainer.ErrUnauthorized}
}
if user == nil {

View File

@ -23,9 +23,18 @@ type Service struct{}
// GetAccessToken takes an access code and exchanges it for an access token from portainer OAuthSettings token endpoint
func (*Service) GetAccessToken(code string, settings *portainer.OAuthSettings) (string, error) {
unescapedCode, err := url.QueryUnescape(code)
if err != nil {
return "", err
}
config := buildConfig(settings)
token, err := config.Exchange(context.Background(), code)
return token.AccessToken, err
token, err := config.Exchange(context.Background(), unescapedCode)
if err != nil {
return "", err
}
return token.AccessToken, nil
}
// GetUsername takes a token and retrieves the portainer OAuthSettings user identifier from resource server.
@ -109,7 +118,6 @@ func buildConfig(oauthSettings *portainer.OAuthSettings) *oauth2.Config {
ClientSecret: oauthSettings.ClientSecret,
Endpoint: endpoint,
RedirectURL: oauthSettings.RedirectURI,
// TODO figure out how to handle different providers, see https://github.com/golang/oauth2/issues/119
Scopes: []string{oauthSettings.Scopes},
Scopes: []string{oauthSettings.Scopes},
}
}

View File

@ -0,0 +1,56 @@
angular.module('portainer.extensions.oauth')
.controller('OAuthProviderSelectorController', function OAuthProviderSelectorController() {
var ctrl = this;
this.providers = [
{
userIdentifier: 'mail',
scope: 'id,email,name',
name: 'microsoft'
},
{
authUrl: 'https://accounts.google.com/o/oauth2/auth',
accessTokenUrl: 'https://accounts.google.com/o/oauth2/token',
resourceUrl: 'https://www.googleapis.com/oauth2/v1/userinfo?alt=json',
userIdentifier: 'email',
scopes: 'profile email',
name: 'google'
},
{
authUrl: 'https://github.com/login/oauth/authorize',
accessTokenUrl: 'https://github.com/login/oauth/access_token',
resourceUrl: 'https://api.github.com/user',
userIdentifier: 'login',
scopes: 'id email name',
name: 'github'
},
{
name: 'custom'
}
];
this.$onInit = onInit;
function onInit() {
console.log(ctrl.provider.authUrl);
if (ctrl.provider.authUrl) {
ctrl.provider = getProviderByURL(ctrl.provider.authUrl);
} else {
ctrl.provider = ctrl.providers[0];
}
ctrl.onSelect(ctrl.provider);
}
function getProviderByURL(providerAuthURL) {
if (providerAuthURL.indexOf('login.microsoftonline.com') !== -1) {
return ctrl.providers[0];
}
else if (providerAuthURL.indexOf('accounts.google.com') !== -1) {
return ctrl.providers[1];
}
else if (providerAuthURL.indexOf('github.com') !== -1) {
return ctrl.providers[2];
}
return ctrl.provider[3];
}
});

View File

@ -1,17 +1,49 @@
<div class="col-sm-12 form-section-title">
Provider
</div>
Provider
</div>
<div class="form-group">
<div class="col-sm-12">
<select
class="form-control"
id="oauth-provider-selector"
ng-model="$ctrl.selectedProvider"
ng-change="$ctrl.onSelect($ctrl.selectedProvider)"
ng-options="provider as provider.name for provider in $ctrl.providers"
>
</select>
<div class="form-group"></div>
<div class="form-group" style="margin-bottom: 0">
<div class="boxselector_wrapper">
<div ng-click="$ctrl.onSelect($ctrl.provider)">
<input type="radio" id="oauth_provider_microsoft" ng-model="$ctrl.provider" ng-value="$ctrl.providers[0]">
<label for="oauth_provider_microsoft">
<div class="boxselector_header">
<i class="fab fa-microsoft" aria-hidden="true" style="margin-right: 2px;"></i>
Microsoft
</div>
<p>Microsoft OAuth provider</p>
</label>
</div>
<div ng-click="$ctrl.onSelect($ctrl.provider)">
<input type="radio" id="oauth_provider_google" ng-model="$ctrl.provider" ng-value="$ctrl.providers[1]">
<label for="oauth_provider_google">
<div class="boxselector_header">
<i class="fab fa-google" aria-hidden="true" style="margin-right: 2px;"></i>
Google
</div>
<p>Google OAuth provider</p>
</label>
</div>
<div ng-click="$ctrl.onSelect($ctrl.provider)">
<input type="radio" id="oauth_provider_github" ng-model="$ctrl.provider" ng-value="$ctrl.providers[2]">
<label for="oauth_provider_github">
<div class="boxselector_header">
<i class="fab fa-github" aria-hidden="true" style="margin-right: 2px;"></i>
Github
</div>
<p>Github OAuth provider</p>
</label>
</div>
<div ng-click="$ctrl.onSelect($ctrl.provider)">
<input type="radio" id="oauth_provider_custom" ng-model="$ctrl.provider" ng-value="$ctrl.providers[3]">
<label for="oauth_provider_custom">
<div class="boxselector_header">
<i class="fa fa-user-check" aria-hidden="true" style="margin-right: 2px;"></i>
Custom
</div>
<p>Custom OAuth provider</p>
</label>
</div>
</div>
</div>

View File

@ -1,20 +1,8 @@
angular.module('portainer.extensions.oauth').component('oauthProvidersSelector', {
templateUrl: 'app/extensions/oauth/components/oauth-providers-selector/oauth-providers-selector.html',
bindings: {
onSelect: '<'
onSelect: '<',
provider: '='
},
controller: function oauthProvidersSelectorController() {
this.providers = [
{
name: 'Facebook',
authUrl: 'https://www.facebook.com/v3.2/dialog/oauth',
accessTokenUrl: 'https://graph.facebook.com/v3.2/oauth/access_token',
resourceUrl: 'https://graph.facebook.com/v3.2/me?fields=email',
userIdentifier: 'email'
},
{
name: 'Custom'
}
];
}
controller: 'OAuthProviderSelectorController'
});

View File

@ -1,11 +1,38 @@
angular.module('portainer.extensions.oauth')
.controller('OAuthSettingsController', function OAuthSettingsController() {
this.onSelectProvider = onSelectProvider;
.controller('OAuthSettingsController', function OAuthSettingsController() {
var ctrl = this;
function onSelectProvider(provider) {
this.settings.AuthorizationURI = provider.authUrl;
this.settings.AccessTokenURI = provider.accessTokenUrl;
this.settings.ResourceURI = provider.resourceUrl;
this.settings.UserIdentifier = provider.userIdentifier;
}
});
this.state = {
provider: {},
overrideConfiguration: false,
microsoftTenantID: ''
};
this.$onInit = onInit;
this.onSelectProvider = onSelectProvider;
this.onMicrosoftTenantIDChange = onMicrosoftTenantIDChange;
function onMicrosoftTenantIDChange() {
var tenantID = ctrl.state.microsoftTenantID;
ctrl.settings.AuthorizationURI = _.replace('https://login.microsoftonline.com/TENANT_ID/oauth2/authorize', 'TENANT_ID', tenantID);
ctrl.settings.AccessTokenURI = _.replace('https://login.microsoftonline.com/TENANT_ID/oauth2/token', 'TENANT_ID', tenantID);
ctrl.settings.ResourceURI = _.replace('https://graph.windows.net/TENANT_ID/me?api-version=2013-11-08', 'TENANT_ID', tenantID);
}
function onSelectProvider(provider) {
ctrl.state.provider = provider;
ctrl.settings.AuthorizationURI = provider.authUrl;
ctrl.settings.AccessTokenURI = provider.accessTokenUrl;
ctrl.settings.ResourceURI = provider.resourceUrl;
ctrl.settings.UserIdentifier = provider.userIdentifier;
ctrl.settings.Scopes = provider.scopes;
}
function onInit() {
if (ctrl.settings.RedirectURI === '') {
ctrl.settings.RedirectURI = window.location.origin;
}
ctrl.state.provider.authUrl = ctrl.settings.AuthorizationURI;
}
});

View File

@ -1,48 +1,61 @@
<div>
<!-- <oauth-providers-selector on-select="$ctrl.onSelectProvider" selected-provider="$ctrl.settings.provider" providers="$ctrl.providers"></oauth-providers-selector> -->
<div class="col-sm-12 form-section-title">
Automatic user provisioning
</div>
<div class="form-group">
<span class="col-sm-12 text-muted small">
With automatic user provisioning enabled, Portainer will create user(s) automatically with standard user role. If
disabled, users must be created in Portainer in order to login.
</span>
</div>
<div class="form-group">
<div class="col-sm-12">
<label for="oauth_provisioning"> Automatic user provisioning </label>
<div class="col-sm-12 form-section-title">
Automatic user provisioning
</div>
<div class="form-group">
<span class="col-sm-12 text-muted small">
With automatic user provisioning enabled, Portainer will create user(s) automatically with standard user role. If
disabled, users must be created in Portainer in order to login.
</span>
</div>
<div class="form-group">
<label class="col-sm-3 col-lg-2 control-label text-left">Automatic user provisioning</label>
<label class="switch" style="margin-left: 20px">
<input type="checkbox" ng-model="$ctrl.settings.OAuthAutoCreateUsers" /><i></i>
</label>
</div>
</div>
<div ng-if="$ctrl.settings.OAuthAutoCreateUsers">
<div class="form-group">
<span class="col-sm-12 text-muted small">
The users created by the automatic provisioning feature can be added to a default team on creation. This setting is optional.
</span>
</div>
<div class="form-group">
<label for="team_provisioning" class="col-sm-2">Default team</label>
<span class="small text-muted" style="margin-left: 20px;" ng-if="$ctrl.teams.length === 0">
You have not yet created any team. Head over the <a ui-sref="portainer.teams">teams view</a> to manage user teams.
</span>
<button type="button" class="btn btn-sm btn-danger" ng-click="$ctrl.settings.DefaultTeamID = null" ng-disabled="!$ctrl.settings.DefaultTeamID"
ng-if="$ctrl.teams.length > 0">
<i class="fa fa-times" aria-hidden="true"></i>
</button>
<div class="col-sm-9 col-lg-9" ng-if="$ctrl.teams.length > 0">
<select class="form-control" ng-model="$ctrl.settings.DefaultTeamID" ng-options="team.Id as team.Name for team in $ctrl.teams">
</select>
<div ng-if="$ctrl.settings.OAuthAutoCreateUsers">
<div class="form-group">
<span class="col-sm-12 text-muted small">
The users created by the automatic provisioning feature can be added to a default team on creation. This setting is optional.
</span>
</div>
<div class="form-group">
<label class="col-sm-3 col-lg-2 control-label text-left">Default team</label>
<span class="small text-muted" style="margin-left: 20px;" ng-if="$ctrl.teams.length === 0">
You have not yet created any team. Head over the <a ui-sref="portainer.teams">teams view</a> to manage user teams.
</span>
<button type="button" class="btn btn-sm btn-danger" ng-click="$ctrl.settings.DefaultTeamID = null" ng-disabled="!$ctrl.settings.DefaultTeamID" ng-if="$ctrl.teams.length > 0"><i class="fa fa-times" aria-hidden="true"></i></button>
<div class="col-sm-9 col-lg-9" ng-if="$ctrl.teams.length > 0">
<select class="form-control" ng-model="$ctrl.settings.DefaultTeamID" ng-options="team.Id as team.Name for team in $ctrl.teams">
<option value="">No team</option>
</select>
</div>
</div>
</div>
</div>
<oauth-providers-selector on-select="$ctrl.onSelectProvider" provider="$ctrl.state.provider"></oauth-providers-selector>
<div class="col-sm-12 form-section-title">OAuth Configuration</div>
<div class="form-group" ng-if="$ctrl.state.provider.name == 'microsoft'">
<label for="oauth_microsoft_tenant_id" class="col-sm-3 col-lg-2 control-label text-left">
Tenant ID
<portainer-tooltip position="bottom" message="ID of the Azure AD directory in which you created the OAuth application"></portainer-tooltip>
</label>
<div class="col-sm-9 col-lg-10">
<input
type="text"
class="form-control"
id="oauth_microsoft_tenant_id"
placeholder="xxxxxxxxxxxxxxxxxxxx"
ng-model="$ctrl.state.microsoftTenantID"
ng-change="$ctrl.onMicrosoftTenantIDChange()"
/>
</div>
</div>
<div class="form-group">
<label for="oauth_client_id" class="col-sm-3 col-lg-2 control-label text-left">
Client ID
@ -50,11 +63,11 @@
</label>
<div class="col-sm-9 col-lg-10">
<input
type="text"
class="form-control"
id="oauth_client_id"
ng-model="$ctrl.settings.ClientID"
placeholder="xxxxxxxxxxxxxxxxxxxx"
type="text"
class="form-control"
id="oauth_client_id"
ng-model="$ctrl.settings.ClientID"
placeholder="xxxxxxxxxxxxxxxxxxxx"
/>
</div>
</div>
@ -63,129 +76,140 @@
<label for="oauth_client_secret" class="col-sm-3 col-lg-2 control-label text-left">
Client Secret
<portainer-tooltip
position="bottom"
message="Client secret that authorization server supports"
position="bottom"
message="Client secret that authorization server supports"
></portainer-tooltip>
</label>
<div class="col-sm-9 col-lg-10">
<input
type="password"
class="form-control"
id="oauth_client_secret"
ng-model="$ctrl.settings.ClientSecret"
placeholder="xxxxxxxxxxxxxxxxxxxx"
type="password"
class="form-control"
id="oauth_client_secret"
ng-model="$ctrl.settings.ClientSecret"
placeholder="xxxxxxxxxxxxxxxxxxxx"
/>
</div>
</div>
<div class="form-group">
<div class="form-group" ng-if="$ctrl.state.provider.name == 'custom' || $ctrl.state.overrideConfiguration">
<label for="oauth_authorization_uri" class="col-sm-3 col-lg-2 control-label text-left">
Authorization URI
<portainer-tooltip
position="bottom"
message="URI where the user is redirected in order to login with OAuth provider"
position="bottom"
message="URI where the user is redirected in order to login with OAuth provider"
></portainer-tooltip>
</label>
<div class="col-sm-9 col-lg-10">
<input
type="text"
class="form-control"
id="oauth_authorization_uri"
ng-model="$ctrl.settings.AuthorizationURI"
placeholder="https://example.com/oauth/authorize"
type="text"
class="form-control"
id="oauth_authorization_uri"
ng-model="$ctrl.settings.AuthorizationURI"
placeholder="https://example.com/oauth/authorize"
/>
</div>
</div>
<div class="form-group">
<div class="form-group" ng-if="$ctrl.state.provider.name == 'custom' || $ctrl.state.overrideConfiguration">
<label for="oauth_access_token_uri" class="col-sm-3 col-lg-2 control-label text-left">
Access Token URI
<portainer-tooltip
position="bottom"
message="URI where portainer will attempt to obtain an access token"
position="bottom"
message="URI where portainer will attempt to obtain an access token"
></portainer-tooltip>
</label>
<div class="col-sm-9 col-lg-10">
<input
type="text"
class="form-control"
id="oauth_access_token_uri"
ng-model="$ctrl.settings.AccessTokenURI"
placeholder="https://example.com/oauth/token"
type="text"
class="form-control"
id="oauth_access_token_uri"
ng-model="$ctrl.settings.AccessTokenURI"
placeholder="https://example.com/oauth/token"
/>
</div>
</div>
<div class="form-group">
<div class="form-group" ng-if="$ctrl.state.provider.name == 'custom' || $ctrl.state.overrideConfiguration">
<label for="oauth_resource_uri" class="col-sm-3 col-lg-2 control-label text-left">
Resource URI
<portainer-tooltip
position="bottom"
message="URI where portainer will attempt to retrieve the user identifier value"
position="bottom"
message="URI where portainer will attempt to retrieve the user identifier value"
></portainer-tooltip>
</label>
<div class="col-sm-9 col-lg-10">
<input
type="text"
class="form-control"
id="oauth_resource_uri"
ng-model="$ctrl.settings.ResourceURI"
placeholder="https://example.com/user"
type="text"
class="form-control"
id="oauth_resource_uri"
ng-model="$ctrl.settings.ResourceURI"
placeholder="https://example.com/user"
/>
</div>
</div>
<div class="form-group">
<div class="form-group" ng-if="$ctrl.state.provider.name == 'custom' || $ctrl.state.overrideConfiguration">
<label for="oauth_redirect_uri" class="col-sm-3 col-lg-2 control-label text-left">
Redirect URI
<portainer-tooltip position="bottom" message="Set this as your portainer index"></portainer-tooltip>
</label>
<div class="col-sm-9 col-lg-10">
<input
type="text"
class="form-control"
id="oauth_redirect_uri"
ng-model="$ctrl.settings.RedirectURI"
placeholder="http://yourportainer.com/"
type="text"
class="form-control"
id="oauth_redirect_uri"
ng-model="$ctrl.settings.RedirectURI"
placeholder="http://yourportainer.com/"
/>
</div>
</div>
<div class="form-group">
<div class="form-group" ng-if="$ctrl.state.provider.name == 'custom' || $ctrl.state.overrideConfiguration">
<label for="oauth_user_identifier" class="col-sm-3 col-lg-2 control-label text-left">
User Identifier
<portainer-tooltip
position="bottom"
message="Key that identifies the user in the resource server request"
position="bottom"
message="Key that identifies the user in the resource server request"
></portainer-tooltip>
</label>
<div class="col-sm-9 col-lg-10">
<input
type="text"
class="form-control"
id="oauth_user_identifier"
ng-model="$ctrl.settings.UserIdentifier"
placeholder="id"
type="text"
class="form-control"
id="oauth_user_identifier"
ng-model="$ctrl.settings.UserIdentifier"
placeholder="id"
/>
</div>
</div>
<div class="form-group">
<div class="form-group" ng-if="$ctrl.state.provider.name == 'custom' || $ctrl.state.overrideConfiguration">
<label for="oauth_scopes" class="col-sm-3 col-lg-2 control-label text-left">
Scopes
<portainer-tooltip
position="bottom"
message="Scopes that are required to obtain the user identifier separated by delimiter if server expects it"
position="bottom"
message="Scopes that are required to obtain the user identifier separated by delimiter if server expects it"
></portainer-tooltip>
</label>
<div class="col-sm-9 col-lg-10">
<input
type="text"
class="form-control"
id="oauth_scopes"
ng-model="$ctrl.settings.Scopes"
placeholder="id,email,name"
type="text"
class="form-control"
id="oauth_scopes"
ng-model="$ctrl.settings.Scopes"
placeholder="id,email,name"
/>
</div>
</div>
<div class="form-group" ng-if="$ctrl.state.provider.name != 'custom'">
<div class="col-sm-12">
<a class="small interactive" ng-if="!$ctrl.state.overrideConfiguration" ng-click="$ctrl.state.overrideConfiguration = true;">
<i class="fa fa-plus space-right" aria-hidden="true"></i> Override configuration
</a>
<a class="small interactive" ng-if="$ctrl.state.overrideConfiguration" ng-click="$ctrl.state.overrideConfiguration = false;">
<i class="fa fa-minus space-right" aria-hidden="true"></i> Hide advanced options
</a>
</div>
</div>
</div>

View File

@ -1,7 +1,8 @@
angular.module('portainer.extensions.oauth').component('oauthSettings', {
templateUrl: 'app/extensions/oauth/components/oauth-settings/oauth-settings.html',
bindings: {
settings: '<',
settings: '=',
teams: '<'
}
},
controller: 'OAuthSettingsController'
});

View File

@ -120,7 +120,7 @@ angular.module('portainer.app').controller('AuthenticationController', ['$q', '$
$state.go('portainer.home');
})
.catch(function error() {
$scope.state.AuthenticationError = 'Failed to authenticate with OAuth2 Provider';
$scope.state.AuthenticationError = 'Unable to login via OAuth';
$scope.state.isInOAuthProcess = false;
});
}

View File

@ -49,7 +49,7 @@
</div>
</div>
</div>
<div ng-if="settings.AuthenticationMethod === 1">
<div class="col-sm-12 form-section-title">
Information
@ -58,7 +58,7 @@
When using internal authentication, Portainer will encrypt user passwords and store credentials locally.
</div>
</div>
<div ng-if="settings.AuthenticationMethod === 2">
<div>
@ -73,7 +73,7 @@
<div class="col-sm-12 form-section-title">
LDAP configuration
</div>
<div class="form-group">
<label for="ldap_url" class="col-sm-3 col-lg-2 control-label text-left">
LDAP Server
@ -325,6 +325,9 @@
<oauth-settings ng-if="isOauthEnabled()" settings="OAuthSettings" teams="teams"></oauth-settings>
<!-- actions -->
<div class="col-sm-12 form-section-title">
Actions
</div>
<div class="form-group">
<div class="col-sm-12">
<button type="button" class="btn btn-primary btn-sm" ng-click="saveSettings()" ng-disabled="state.actionInProgress" button-spinner="state.actionInProgress">