Skip to content

Commit

Permalink
Merge pull request #2 from scc-digitalhub/oauth-updates
Browse files Browse the repository at this point in the history
Merge oauth updates
  • Loading branch information
matteo-s authored Nov 9, 2020
2 parents 6ef1ad6 + 5aaad43 commit 916d56b
Show file tree
Hide file tree
Showing 15 changed files with 1,068 additions and 498 deletions.
152 changes: 131 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,33 +37,143 @@ Note that the detailed installation procedure is only summarized here and is bet

Now Cyclotron is running with its default settings and authentication is disabled. Proceed to configure authentication via AAC.

## Authentication Configuration with AAC
## Authentication Configuration with OAuth2

Open `cyclotron-svc/config/config.js` and update the properties according to your needs (remember to configure the same properties in the website config file, e.g. the API server URL. To use AAC as authentication provider, be sure to set the following properties with the correct AAC URLs:
This fork supports OAuth2/OpenID as login method, via *implicit flow*.
The module is usable with any *OAuth2/OIDC-compliant* identity provider, but some advanced functionalities such as role mapping and permission evaluators are available only when using **AAC** as IdP.

Remember to set the same configuration (when needed) to both backend and frontend, without exposing private variables.

### Backend configuration
Open `cyclotron-svc/config/config.js` and update the properties according to your needs.
```
enableAuth: true,
authProvider: 'aac',
oauth: {
userProfileEndpoint: 'http://localhost:8080/aac/basicprofile/me',
userRolesEndpoint: 'http://localhost:8080/aac/userroles/me',
scopes: 'profile.basicprofile.me,user.roles.me',
tokenValidityEndpoint: 'http://localhost:8080/aac/resources/access',
tokenInfoEndpoint: 'http://localhost:8080/aac/resources/token',
tokenRolesEndpoint: 'http://localhost:8080/aac/userroles/token',
apikeyCheckEndpoint: 'http://localhost:8080/aac/apikeycheck',
parentSpace: 'components/cyclotron'
}
useJWT: <true|false>,
clientId: '<clientId>',
clientSecret: '<clientSecret>',
jwksEndpoint: 'http://localhost:8080/aac/jwk',
tokenIntrospectionEndpoint: 'http://localhost:8080/aac/oauth/introspect',
userProfileEndpoint: 'http://localhost:8080/aac/userinfo',
parentSpace: 'components/cyclotron',
editorRoles: ['ROLE_PROVIDER','ROLE_EDITOR']
},
```

Open `cyclotron-site/_public/js/conf/configService.js` and update it too. Be sure to set the following properties under `authentication` (you will set the client ID later):

enable: true,
authProvider: 'aac',
authorizationURL: 'http://localhost:8080/aac/eauth/authorize',
clientID: '',
callbackDomain: 'http://localhost:8088',
scopes: 'profile.basicprofile.me user.roles.me',
userProfileEndpoint: 'http://localhost:8080/aac/basicprofile/me',
tokenValidityEndpoint: 'http://localhost:8080/aac/resources/access'
In order to use **JWTs** as bearer tokens, and locally verify them, please set ``useJWT:true`` and provide only one of these two configurations:

* populate ``jwksEndpoint`` with the JWKS uri to use public/private key verification via RSA
* set ``clientSecret`` and leave ``jwjwksEndpoint`` empty to use simmetric HMAC with clientSecret as key

Examples:

```
#JWT + public RSA key
oauth: {
useJWT: true,
clientId: '<clientId>',
clientSecret: '',
jwksEndpoint: 'http://localhost:8080/aac/jwk',
tokenIntrospectionEndpoint: '',
userProfileEndpoint: '',
},
#JWT + private HMAC key
oauth: {
useJWT: true,
clientId: '<clientId>',
clientSecret: '<clientSecret>',
jwksEndpoint: '',
tokenIntrospectionEndpoint: '',
userProfileEndpoint: '',
},
```

Do note that the default validation will check for a valid signature and for the correspondence between ``clientId`` and ``audience``.
If you want to also validate the *issuer* of JWT tokens set the corresponding property in config:
```
oauth: {
issuer: <issuer>
}
```


Alternatively, you can use **opaque tokens** as bearer, and thus leverage *OAuth2 introspection* plus *OpenID userProfile*. This configuration requires ``useJWT:false`` and all the endpoints properly populated (except ``jwksEndpoint``).

Example:
```
#opaque oauth
oauth: {
useJWT: false,
clientId: '<clientId>',
clientSecret: '<clientSecret>',
jwksEndpoint: '',
tokenIntrospectionEndpoint: 'http://localhost:8080/aac/oauth/introspect',
userProfileEndpoint: 'http://localhost:8080/aac/userinfo',
},
```

### Role mapping
By default, valid users are given the permission to create and manage their own dashboards, but can not access private dashboard without a proper role.

Cyclotron supports two different roles:
* ``viewers``
* ``editors``

When using an external IdP (such as AAC) it is possible to map ``roles`` and ``groups`` by defining a mapping for the **editor** role and a context for the component space.

As such, it is possible to dynamically assign roles to users at login, by deriving their group membership from the IdP user profile.

```
oauth: {
parentSpace: 'components/cyclotron',
editorRoles: ['ROLE_PROVIDER','ROLE_EDITOR']
},
```
By setting the ``parentSpace`` we define a prefix for roles obtained from the IdP, which is then used to derive the ``group`` from the following pattern:

```
<parentSpace>/<groupName>:<roleName>
```
For example, the upstream role ``components/cyclotron/testgroup:ROLE_PROVIDER`` with the given configuration can be translated to:

* (``parentSpace=components/cyclotron``)
* ``group=testgroup``
* ``role=editor``

because the upstream ``ROLE_PROVIDER`` role is recognized as an editor role.

An upstream role as ``components/cyclotron/testgroup:ROLE_USER`` will be translated as

* (``parentSpace=components/cyclotron``)
* ``group=testgroup``
* ``role=viewer``

Without a direct mapping to a given group, the system won't assign any role to the current user in such group. The user won't thus be able to access any dashboard restricted to the group.



### Frontend configuration
Open `cyclotron-site/_public/js/conf/configService.js` and update it too. Be sure to set the following properties under `authentication`:

```
authentication: {
enable: true,
authProvider: 'aac',
authorizationURL: 'http://localhost:8080/aac/eauth/authorize',
clientID: '<clientID>',
callbackDomain: 'http://localhost:8088',
scopes: 'openid profile user.roles.me'
}
```


## Client Application Configuration on AAC

Expand All @@ -74,7 +184,7 @@ Log in to AAC as a provider user and click "New App" to create a client applicat

In the API Access tab:

* under Basic Profile Service, check `profile.basicprofile.me` to give access to user profiles to the client app
* under Basic Profile Service, check ``openid`` and `profile.basicprofile.me` to give access to user profiles to the client app
* under Role Management Service, check `user.roles.me` to give access to user roles

In the Overview tab, copy `clientId` property, then go back to `cyclotron-site/_public/js/conf/configService.js` and add it in the `authentication` section.
Expand Down
46 changes: 34 additions & 12 deletions conf/cyclotron/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ module.exports = {
/* URL for website using this service
* Used for exporting Dashboards as PDFs via CasperJS
*/
webServer: 'http://localhost:777',
webServer: 'http://localhost:7777',

/* Key for encrypting/decrypting strings on the /crypto endpoint */
encryptionKey: '',
Expand Down Expand Up @@ -79,20 +79,42 @@ module.exports = {
]
},

/* AAC provider configuration, if authentication is enabled.
* Scopes must be comma-separated.
*/
/* OAuth2 provider configuration, if authentication is enabled.
* to enable JWT set jwks uri + clientId
* to use introspection set endpoints + clientId + clientSecret
*/
oauth: {
userProfileEndpoint: 'http://aac:8080/aac/basicprofile/me',
userRolesEndpoint: 'http://aac:8080/aac/userroles/me',
scopes: 'profile.basicprofile.me,user.roles.me',
tokenValidityEndpoint: 'http://aac:8080/aac/resources/access',
tokenInfoEndpoint: 'http://aac:8080/aac/resources/token',
tokenRolesEndpoint: 'http://aac:8080/aac/userroles/token',
apikeyCheckEndpoint: 'http://aac:8080/aac/apikeycheck',
parentSpace: 'components/cyclotron'
clientId: '480466f2-53ea-41f9-8d05-0a1e01482aea',
clientSecret: '***',
jwksEndpoint: '',
tokenIntrospectionEndpoint: 'http://localhost:8080/aac/oauth/introspect',
userProfileEndpoint: 'http://localhost:8080/aac/userinfo',
parentSpace: 'components/cyclotron',
editorRoles: ['ROLE_PROVIDER', 'ROLE_EDITOR']
},

/*
* API Key support
*/
apikey: {
apikeyCheckEndpoint: 'http://localhost:8080/aac/apikeycheck'
},


// /* AAC provider configuration, if authentication is enabled.
// * Scopes must be comma-separated.
// */
// oauth: {
// userProfileEndpoint: 'http://172.17.0.1:9090/aac/basicprofile/me',
// userRolesEndpoint: 'http://172.17.0.1:9090/aac/userroles/me',
// scopes: 'profile.basicprofile.me,user.roles.me',
// tokenValidityEndpoint: 'http://172.17.0.1:9090/aac/oauth/introspect',
// tokenInfoEndpoint: 'http://172.17.0.1:9090/aac/oauth/introspect',
// tokenRolesEndpoint: 'http://172.17.0.1:9090/aac/userroles/token',
// apikeyCheckEndpoint: 'http://172.17.0.1:9090/aac/apikeycheck',
// parentSpace: 'components/cyclotron'
// },

/* List of LDAP distinguished names for Admin users
* These user(s) can override normal permission settings
* Only used if enableAuth is true
Expand Down
12 changes: 12 additions & 0 deletions cyclotron-site/app/scripts/common/app.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,11 @@ cyclotronApp.config ($stateProvider, $urlRouterProvider, $locationProvider, $con
userService.storeAccessToken()
]

# Store api key
storeApiKey = ['userService', (userService) ->
userService.storeApiKey()
]

#
# Application Router
#
Expand Down Expand Up @@ -302,6 +307,13 @@ cyclotronApp.config ($stateProvider, $urlRouterProvider, $locationProvider, $con
resolve:
token: storeAccessToken
})
.state('apikeyCallback', {
url: '/apikey=:apikey'
data:
title: 'Cyclotron'
resolve:
apiKey: storeApiKey
})
.state('dashboard', {
url: '/{dashboard:.+}'
templateUrl: '/partials/dashboard.html'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,14 +113,16 @@ cyclotronServices.factory 'userService', ($http, $localForage, $q, $rootScope, $
#TODO possible check for apikey: $location.search().apikey != undefined

deferred = $q.defer()
errorHandler = ->
errorHandler = (error) ->
console.log("got error "+error)
exports.setLoggedOut()
deferred.resolve(null)

if configService.authentication.enable == true

$localForage.getItem('session').then (existingSession) ->

console.log("got from forage session")
console.dir(existingSession)
if existingSession?
validator = $http.post(configService.restServiceUrl + '/users/validate', { key: existingSession.key })
validator.success (session) ->
Expand All @@ -133,12 +135,12 @@ cyclotronServices.factory 'userService', ($http, $localForage, $q, $rootScope, $
validator.error (error) ->
$localForage.removeItem('session')
alertify.log('Previous session expired', 2500) unless hideAlerts
errorHandler()
errorHandler(error)
else
errorHandler()
errorHandler('missing existing session')
, errorHandler
else
errorHandler()
errorHandler('auth disabled')

return deferred.promise

Expand Down Expand Up @@ -289,4 +291,55 @@ cyclotronServices.factory 'userService', ($http, $localForage, $q, $rootScope, $
deferred.reject(error)
$location.path('/').replace()



exports.storeApiKey = () ->
hash = $location.path().substr(1)
params = hash.split('&')
keyProperties = {}

_.each params, (value) ->
keyValuePair = value.split('=')
key = keyValuePair[0]
val = keyValuePair[1]
keyProperties[key] = val

deferred = $q.defer()

get = $http({
method: 'GET'
url: configService.restServiceUrl + '/users/apikey?apikey=' + keyProperties.apikey
})

get.success (session) ->
currentSession = session

# Store session and username in localstorage
$localForage.setItem 'session', session
$localForage.setItem 'username', session.user.sAMAccountName
$localForage.setItem 'cachedUserId', session.user._id
exports.cachedUsername = session.user.sAMAccountName
exports.cachedUserId = session.user._id

loggedIn = true

$rootScope.$broadcast 'login', { }
if $window.Cyclotron?
$window.Cyclotron.currentUser = session.user
alertify.success('Logged in as <strong>' + session.user.name + '</strong>', 2500)

deferred.resolve(session)
#finally redirect to last url
$localForage.getItem('urlBeforeLogin').then (urlBeforeLogin) ->
if urlBeforeLogin?
$location.url(urlBeforeLogin).replace()
else
$location.path('/').replace()

get.error (error) ->
console.log 'error or API sent an error response', error
exports.setLoggedOut()
deferred.reject(error)
$location.path('/').replace()

return exports
8 changes: 8 additions & 0 deletions cyclotron-svc/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,14 @@ if (config.enableAuth == true) {
var session = require('./middleware/session');
app.use(session.sessionLoader);

/* Oauth session management */
var oauth = require('./middleware/oauth');
app.use(oauth.oauthLoader);

/* Apikey session management */
var apikey = require('./middleware/apikey');
app.use(apikey.apikeyLoader);

/* Passport.js LDAP authentication */
var passport = require('passport'),
LdapStrategy = require('passport-ldapauth');
Expand Down
Loading

0 comments on commit 916d56b

Please sign in to comment.