1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-07-23 07:19:41 +02:00

feat(oauth): update OAuth UX

This commit is contained in:
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 { 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 { 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 // 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) { func (*Service) GetAccessToken(code string, settings *portainer.OAuthSettings) (string, error) {
unescapedCode, err := url.QueryUnescape(code)
if err != nil {
return "", err
}
config := buildConfig(settings) config := buildConfig(settings)
token, err := config.Exchange(context.Background(), code) token, err := config.Exchange(context.Background(), unescapedCode)
return token.AccessToken, err if err != nil {
return "", err
}
return token.AccessToken, nil
} }
// GetUsername takes a token and retrieves the portainer OAuthSettings user identifier from resource server. // 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, ClientSecret: oauthSettings.ClientSecret,
Endpoint: endpoint, Endpoint: endpoint,
RedirectURL: oauthSettings.RedirectURI, 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

@ -2,16 +2,48 @@
Provider Provider
</div> </div>
<div class="form-group"></div>
<div class="form-group"> <div class="form-group" style="margin-bottom: 0">
<div class="col-sm-12"> <div class="boxselector_wrapper">
<select <div ng-click="$ctrl.onSelect($ctrl.provider)">
class="form-control" <input type="radio" id="oauth_provider_microsoft" ng-model="$ctrl.provider" ng-value="$ctrl.providers[0]">
id="oauth-provider-selector" <label for="oauth_provider_microsoft">
ng-model="$ctrl.selectedProvider" <div class="boxselector_header">
ng-change="$ctrl.onSelect($ctrl.selectedProvider)" <i class="fab fa-microsoft" aria-hidden="true" style="margin-right: 2px;"></i>
ng-options="provider as provider.name for provider in $ctrl.providers" Microsoft
> </div>
</select> <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>
</div> </div>

View file

@ -1,20 +1,8 @@
angular.module('portainer.extensions.oauth').component('oauthProvidersSelector', { angular.module('portainer.extensions.oauth').component('oauthProvidersSelector', {
templateUrl: 'app/extensions/oauth/components/oauth-providers-selector/oauth-providers-selector.html', templateUrl: 'app/extensions/oauth/components/oauth-providers-selector/oauth-providers-selector.html',
bindings: { bindings: {
onSelect: '<' onSelect: '<',
provider: '='
}, },
controller: function oauthProvidersSelectorController() { controller: 'OAuthProviderSelectorController'
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'
}
];
}
}); });

View file

@ -1,11 +1,38 @@
angular.module('portainer.extensions.oauth') angular.module('portainer.extensions.oauth')
.controller('OAuthSettingsController', function OAuthSettingsController() { .controller('OAuthSettingsController', function OAuthSettingsController() {
var ctrl = this;
this.state = {
provider: {},
overrideConfiguration: false,
microsoftTenantID: ''
};
this.$onInit = onInit;
this.onSelectProvider = onSelectProvider; 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) { function onSelectProvider(provider) {
this.settings.AuthorizationURI = provider.authUrl; ctrl.state.provider = provider;
this.settings.AccessTokenURI = provider.accessTokenUrl; ctrl.settings.AuthorizationURI = provider.authUrl;
this.settings.ResourceURI = provider.resourceUrl; ctrl.settings.AccessTokenURI = provider.accessTokenUrl;
this.settings.UserIdentifier = provider.userIdentifier; 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,5 +1,4 @@
<div> <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"> <div class="col-sm-12 form-section-title">
Automatic user provisioning Automatic user provisioning
</div> </div>
@ -10,13 +9,11 @@
</span> </span>
</div> </div>
<div class="form-group"> <div class="form-group">
<div class="col-sm-12"> <label class="col-sm-3 col-lg-2 control-label text-left">Automatic user provisioning</label>
<label for="oauth_provisioning"> Automatic user provisioning </label>
<label class="switch" style="margin-left: 20px"> <label class="switch" style="margin-left: 20px">
<input type="checkbox" ng-model="$ctrl.settings.OAuthAutoCreateUsers" /><i></i> <input type="checkbox" ng-model="$ctrl.settings.OAuthAutoCreateUsers" /><i></i>
</label> </label>
</div> </div>
</div>
<div ng-if="$ctrl.settings.OAuthAutoCreateUsers"> <div ng-if="$ctrl.settings.OAuthAutoCreateUsers">
<div class="form-group"> <div class="form-group">
@ -25,24 +22,40 @@
</span> </span>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="team_provisioning" class="col-sm-2">Default team</label> <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"> <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. You have not yet created any team. Head over the <a ui-sref="portainer.teams">teams view</a> to manage user teams.
</span> </span>
<button type="button" class="btn btn-sm btn-danger" ng-click="$ctrl.settings.DefaultTeamID = null" ng-disabled="!$ctrl.settings.DefaultTeamID" <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>
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"> <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 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> </select>
</div> </div>
</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="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"> <div class="form-group">
<label for="oauth_client_id" class="col-sm-3 col-lg-2 control-label text-left"> <label for="oauth_client_id" class="col-sm-3 col-lg-2 control-label text-left">
Client ID Client ID
@ -78,7 +91,7 @@
</div> </div>
</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"> <label for="oauth_authorization_uri" class="col-sm-3 col-lg-2 control-label text-left">
Authorization URI Authorization URI
<portainer-tooltip <portainer-tooltip
@ -97,7 +110,7 @@
</div> </div>
</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"> <label for="oauth_access_token_uri" class="col-sm-3 col-lg-2 control-label text-left">
Access Token URI Access Token URI
<portainer-tooltip <portainer-tooltip
@ -116,7 +129,7 @@
</div> </div>
</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"> <label for="oauth_resource_uri" class="col-sm-3 col-lg-2 control-label text-left">
Resource URI Resource URI
<portainer-tooltip <portainer-tooltip
@ -135,7 +148,7 @@
</div> </div>
</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"> <label for="oauth_redirect_uri" class="col-sm-3 col-lg-2 control-label text-left">
Redirect URI Redirect URI
<portainer-tooltip position="bottom" message="Set this as your portainer index"></portainer-tooltip> <portainer-tooltip position="bottom" message="Set this as your portainer index"></portainer-tooltip>
@ -151,7 +164,7 @@
</div> </div>
</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"> <label for="oauth_user_identifier" class="col-sm-3 col-lg-2 control-label text-left">
User Identifier User Identifier
<portainer-tooltip <portainer-tooltip
@ -170,7 +183,7 @@
</div> </div>
</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"> <label for="oauth_scopes" class="col-sm-3 col-lg-2 control-label text-left">
Scopes Scopes
<portainer-tooltip <portainer-tooltip
@ -188,4 +201,15 @@
/> />
</div> </div>
</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> </div>

View file

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

View file

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

View file

@ -325,6 +325,9 @@
<oauth-settings ng-if="isOauthEnabled()" settings="OAuthSettings" teams="teams"></oauth-settings> <oauth-settings ng-if="isOauthEnabled()" settings="OAuthSettings" teams="teams"></oauth-settings>
<!-- actions --> <!-- actions -->
<div class="col-sm-12 form-section-title">
Actions
</div>
<div class="form-group"> <div class="form-group">
<div class="col-sm-12"> <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"> <button type="button" class="btn btn-primary btn-sm" ng-click="saveSettings()" ng-disabled="state.actionInProgress" button-spinner="state.actionInProgress">