This page describes the details of the security implementation in WebAPI.
The security subsystem is built on top of Apache Shiro framework.
Apache Shiro is a powerful and easy-to-use Java security framework that performs authentication, authorization, cryptography, and session management. With Shiro’s easy-to-understand API, you can quickly and easily secure any application – from the smallest mobile applications to the largest web and enterprise applications.
It also uses:
The org.ohdsi.webapi.shiro.management.Security
abstraction makes it easy to maintain different behaviors of security subsystem. There are two implementations are available out of the box. These are AtlasSecurity
and DisabledSecurity
. The first handles all the needs of ATLAS application, the latter disables security features.
The default setting in the WebAPI pom.xml is <security.enabled>false</security.enabled>
which turns off security by loading the DisabledSecurity module. If you would like to enable security and load the AtlasSecurity module, this can be done by adding <security.enabled>true</security.enabled>
to the <profile>
section of your settings.xml file as described in the WebAPI Installation Guide. This does require that you rebuild the .war file and redeploy the application.
The security subsystem is set up by assigning filters to URLs. Before a method underneath the URL is invoked, filters apply some logic. For example, attempt to authenticate user and check if user has permission required to access the method.
WebAPI itself does not authenticate users, it delegates authentication to external service - Windows Authentication or OAuth provider.
There is a set of authenticating filters which allows to maintain different authentication providers. If authentication is successful, WebAPI responds with token, which then should be used to sign subsequent requests. The token is sent in Bearer
header or as a path parameter in case of OAuth.
To sign request put the token prefixed with Bearer
keyword into Authorization
header
Authorization: Bearer mytokenvalue
The token is JSON Web Token. It is used not only as a proof if identity but also contains some useful information, such as login and user’s permissions. It allows to check permissions on client side without need of further communication with WebAPI.
Here is a list of endpoints related to authentication:
/user/login
- Windows Authentication endpoint, responds with token within Bearer
header/user/oauth/google
- Google OAuth, redirects to configured endpoint, putting token as a path parameter/user/oauth/facebook
- Facebook OAuth, redirects to configured endpoint, putting token as a path parameter/user/logout
- Invalidates token, request should be signed with valid token/user/refresh
- Invalidates current token and creates new one with updated permissions and expiration time, request should be signed with valid tokenCurrently supported OAuth providers are Google and Facebook.
To be able to use API of OAuth service provider you need to obtain API Key and API Secret and put these values into POM file
<security.oauth.google.apiKey>KEY</security.oauth.google.apiKey> <security.oauth.google.apiSecret>SECRET</security.oauth.google.apiSecret> <security.oauth.facebook.apiKey>KEY</security.oauth.facebook.apiKey> <security.oauth.facebook.apiSecret>SECRET</security.oauth.facebook.apiSecret>
OAuth authentication is handled with buji-pac4j OAuth clients.
To add support of new OAuth service provider into WebAPI, you need to go through following steps.
/user/oauth/callback
. The same endpoint is used for all OAuth clients. To distinguish which client is responded add client_name
parameter to the path. For example, https://hixbeta.jnj.com:8443/WebAPI/user/oauth/callback?client_name=FacebookClient
for Facebook.public class AtlasSecurity extends Security { ... @Override public Map<String, Filter> getFilters() { Map<String, javax.servlet.Filter> filters = new HashMap<>(); ... // create client FacebookClient facebookClient = new FacebookClient(facebookApiKey, facebookApiSecret); facebookClient.setScope("email"); facebookClient.setFields("email"); Config cfg = new Config( new Clients( this.oauthApiCallback , googleClient , facebookClient // update config to use new client )); // create filter SecurityFilter facebookOauthFilter = new SecurityFilter(); facebookOauthFilter.setConfig(cfg); facebookOauthFilter.setClients("FacebookClient"); filters.put("facebookAuthc", facebookOauthFilter); ... return filters; } ... }
public class AtlasSecurity extends Security { ... @Override public Map<String, String> getFilterChain() { return new FilterChainBuilder() ... .addOAuthPath("/user/oauth/facebook", "facebookAuthc") ... .build(); } ... }
<security.oauth.callback.ui>http://localhost:8080/Atlas/#/welcome</security.oauth.callback.ui>
If authentication completes successfully, token is appended to the redirect URL.
In the event that you do not have one of the supported OAuth providers available, WebAPI also supports as basic security configuration as described in this tutorial.
To access protected method, user must have an appropriate permission. WebAPI checks permissions which are built based on method’s URL and HTTP Method of the request.
For example, request
GET: myservice/myresource/10
corresponds to permission
myservice:myresource:10:get
However, user doesn’t need to have exactly this permission, because SHIRO provides wildcard permissions. It means, that permissions are multilevel and each level may be replaced with a wildcard. Levels are separated by the colon sign and the asterisk sign is used as wildcard character.
For the example above, user may access to resource if he or she has one of the following permissions (assuming that 10 is entity’s ID)
*
- access to everythingmyservice
- access to all methods of myservice
myservice:myresource:*:get
- access to any entity of myresource
myservice:myresource:10:get
- access to entity with ID = 10So that it’s easy to change level of granularity by giving permissions to users for a whole services or just for certain entities or whatever makes sense.
For details on wildcard permissions please refer to https://shiro.apache.org/static/1.2.3/apidocs/org/apache/shiro/authz/permission/WildcardPermission.html
To protect certain method, assign appropriate filters to its URL. There is a helper FilterChainBuilder class, which simplifies this. All you need is just to call addProtectedRestPath
method passing there URL pattern.
Here is an example of how to protect all methods of user
service and generate
method of cohortdefinition
service
public class AtlasSecurity extends Security { ... @Override public Map<String, String> getFilterChain() { return new FilterChainBuilder() ... .addProtectedRestPath("/user/**") .addProtectedRestPath("/cohortdefinition/*/generate/*") ... .build(); } ... }
Now request to all endpoints which matching these patterns should be signed with bearer token (see AUTHENTICATION section) and requires permissions corresponding to endpoint's URL (see AUTHORIZAION section).
To protect all methods, use pattern which matches all endpoints:
public class AtlasSecurity extends Security { ... @Override public Map<String, String> getFilterChain() { return new FilterChainBuilder() ... .addProtectedRestPath("/**") .build(); } ... }
Permissions are not directly assigned to user. Instead, they are grouped into roles and then roles are assigned to user. This makes access control flexible. You may think about it in terms of roles which user plays in your infrastructure. If user's responsobilities were changed, you can move he or she into another role. Or you can add permissions into role if you realized that this role should involve new activities. All you need is just update records in database by calling appropriate WebAPI method.
Here is a list of tables which security subsystem uses to handle access control:
When user logs in firts time, record is created in SEC_USER table. Now user is registered and can be associated with roles.
There are two special roles - public
role and personal role. Both are assigned to every newly registred users.
One personal role is created for each new user. The name of personal role is the same as user's login.
This role is intended to hold permissions which are specific to certain user. For example, when user creates new entity, it may be usefull to restrict access to methods which affect the entity, so that only author can change or delete it.
This workflow works when user creates Concept Set, Cohort or Role. Methods which affect these entities are protected. When entity is created, entity-level permissions required for these methods are created as well and then are assigned to creator's personal role.
Public
role is intended to hold permissions which should have all users. Initially this role does not contain any permissions, but preveliged users can add permissions into it, so that such permissions will be assigned to all current and future users.
When WebAPI starts first time, it creates a set of roles and permissions. Here is a list:
public
- intended to contain permissions granted to all users (empty by default)admin
- contains permissions required to manage roles and permissions and to add permissions into public
roleconcept set creator
- contains permissions required to create concept setscohort reader
- contains permissions required to read cohortscohort creator
- contains permissions required to create cohorts
Each time when WebAPI starts, it looks for configured sources and creates appropriate permissions and roles. One role is created for each source. Roles are named like Source user ({SourceKey})
, where {SourceKey}
is a value of source.source_key column. For example Source user (CCAE_DEV)
.
Settings are available through POM file. All settings related to security are prefixed with security
. Here is a full list:
security.origin
- set client application's origin to make WepAPI methods available from within JavaScript.security.token.expiration
- token expiration time in secondssecurity.ssl.enabled
- if set to true
only HTTPS requests will be processedsecurity.ssl.port
- port for SSL connectionsecurity.oauth.callback.ui
- URL of page in client application to redirect after OAuth authentication is completedsecurity.oauth.callback.api
- URL of WebAPI's endpoint to handle response from OAuth provider. Should end with /user/oauth/callback
.security.oauth.google.apiKey
- Google OAuth API keysecurity.oauth.google.apiSecret
- Google OAuth API secretsecurity.oauth.facebook.apiKey
- Facebook OAuth API keysecurity.oauth.facebook.apiSecret
- Facebook OAuth API secretFirst you will need to obtain and install a certificate. Here are steps to achive this:
keytool
utillitykeytool -genkey -alias webapi -keyalg RSA -keystore C:\path\to\my\keystore.jks -keysize 2048
keytool -certreq -alias webapi -keystore C:\path\to\my\keystore.jks -file C:\path\to\csr\webapi.csr
You will perform the same steps as above in step 1 but with the middle certificate (Intermediate).
You should now have three files:
Import the Root Certificate first. You will specify your own alias for this import Example: Root.
keytool -import -alias root -trustcacerts -file C:\path\to\root.cer -keystore C:\path\to\my\keystore.jks
Import the Intermediate CA certificate second. You will specify your own alias for this import. Example: Intermediate.
keytool -import -alias intermediate -trustcacerts -file C:\path\to\intermediate.cer -keystore C:\path\to\my\keystore.jks
Lastly, import the actual SSL certificate into the keystore.
keytool -importcert -trustcacerts -alias webapi -file C:\path\to\cert\webapi.p7b -keystore C:\path\to\my\keystore.jks
Now you can add SSL connector in Tomcat's server.xml file
<Connector port="8443" protocol="org.apache.coyote.http11.Http11NioProtocol" maxThreads="200" scheme="https" secure="true" SSLEnabled="true" keyAlias="webapi" keystoreFile="C:\path\to\my\keystore.jks" keystorePass="{Your keystore password}" clientAuth="false" sslProtocol="TLS"/>
Here you may find more details on SSL configuration in Tomcat.
When user accesses some sections of ATLAS or press some buttons it results in calling WebAPI methods. Some of them are protected and user may not have appropriate permission. In this case error is generated. To prevent this ATLAS shows UI controls based on user’s permissions.
If user tries to access protected section and he or she isn’t authenticated, the following screen will be shown
If user is authenticated but doesn’t have permissions required for the section, the following screen will be shown
Also some UI controls (such as buttons, checkboxes, etc.) are disabled if user isn’t permitted for corresponding action. For example, if user isn’t permitted to create cohort, the “New Cohort” button is disabled.
Some UI controls may correspond to set of WebAPI calls. To ensure that enabling this UI control is safe permissions must be checked for all required methods. For example, to update Concept Set ATLAS performs two requests
POST: conceptset/{conceptsetId} POST: conceptset/{conceptsetId}/items
where {conceptsetId}
is ID of updated Concept Set.
To address this AuthAPI.js contains set of isPermitted… methods to check all necessary permissions required to perform an atomic (in terms of ATLAS) actions. For the above example it is
isPermittedUpdateConceptset(conceptsetId)
AuthAPI.js also contains isAuthenticated()
method to check if access token is presented and not expired.
Note that these permission checks are performed against information contained in the token itself and it may be outdated at the time of checking. This means, that there is still a chance that ATLAS request will be refused by WebAPI with Unauthorized
or Forbidden
error. To handle such errors, you may use handleAccessDenied(error)
method
$.ajax({ ... error: authApi.handleAccessDenied, ... });
To update the token use refreshToken()
method.
Client application should sign request with access token to be able to call protected methods. This means that Authorization
header must be set. To obtain valid Authorization
header use getAuthorizationHeader()
method of AuthAPI.js.
In case of JQuery AJAX call it may look like this
$.ajax({ ... headers: { Authorization: authApi.getAuthorizationHeader() }, ... });
Permissions are grouped into roles. Administrator can assign role to user. On ‘Configuration’ page you may find ‘Manage Permissions’ button. It opens ‘Roles’ page.
Note that ‘Configuration’ page available only for members of ‘admin’ role.
You may click on certain role to edit it or press ‘New Role’ button to create new role.
Now you're on 'Role' page. Select users which are participated in the role on ‘Users’ tab.
To define permissions for role members, go to ‘Permissions’ tab.
Note that users can’t create permissions - all necessary permissions are created automatically.