Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

...

Info

For web services and UI authentication for Grouper 2v2.5 and above see this page 


High level Grouper

...

Web Services authentication

From a high level, grouper or the servlet container or web server or servlet filter will authenticate the user, and then the user needs to be resolved into a Subject.

Note the default and most popular authentication protocol in grouper-ws is http-basic (Authorization header), so for this and other reasons make sure your deployments of grouper-ws are protected with SSL.  In the v2.5+ container there is a param to enable self-signed SSL certs for quick starts if you dont have a real certificate yet.

Types of authentication

...

  1. Self-service JWT
  2. JWT from trusted authority
  3. Grouper built-in basic authentication
  4. Grouper LDAP authentication
  5. Tomcat authentication
  6. Apache authentication
  7. Tomcat: tomcat-users.xml (holds user/pass)
  8. Apache htpasswd
  9. Apache basic with ldap
  10. Kerberos user/pass
  11. SSL certificatesCustom authentication
  12. Rampart (on top of something else)
  13. SSL certificates.  There is no documentation on this.  Do this with Apache or Tomcat and pass in the REMOTE_USER.
  14. other?  if you can get a REMOTE_USER via apache or tomcat plugin or filter, it will work with Grouper WS

Types of subject sources

When the authentication happens and the principal name is given to Grouper from the authentication source, it needs to be resolvable as a subject.  There are a few options

  1. Use your existing subject source (e.g. ldap, if it can it hold accounts that represent systems?)
  2. Grouper Local Entities
  3. Custom SQL table and source

Grouper Local entities

You can link up an account with a local entity.  Here is an example from the demo server

Add a user via apache like the previous link: test_local_entity

Code Block
[mchyzer@i2midev1 ~]$ sudo htpasswd /etc/httpd/conf.d/users.pass test_local_entity
New password: 
Re-type new password: 
Adding password for user test_local_entity

Create the local entity in Grouper

Image Removed

Add it to the WS group (note, new UI cant add local entities up to 2.3.0, you need to use lite ui)

Image Removed

configure a prefix on logins on WS: in grouper-ws.properties

Code Block
# prepend to the userid this value (e.g. if using local entities, might be:    etc:servicePrincipals:   )
ws.security.prependToUserIdForSubjectLookup = etc:servicePrincipals:

Hit a link:  login as   test_local_entity   and    whatever_pass

https://grouperdemo.internet2.edu/grouper-ws_v2_3/servicesRest/json/v2_3_000/groups/test%3AtestGroup/members

Custom SQL table and source

Note, you can use the built in grouper sql source like the grouper demo server

Here is an example of custom sql table and source used at Penn with Oracle

Table:

Code Block
CREATE TABLE SERVICE_PRINCIPALS
(
  PRINCIPAL_NAME     VARCHAR2(512 CHAR),
  ID                 INTEGER,
  LAST_UPDATED       TIMESTAMP(6),
  REASON             VARCHAR2(4000 CHAR),
  PENNKEY_WHO_ADDED  CHAR(20 CHAR),
  PENNID_WHO_ADDED   VARCHAR2(20 CHAR)
);
COMMENT ON COLUMN SERVICE_PRINCIPALS.REASON IS 'reason for adding this principal';
COMMENT ON COLUMN SERVICE_PRINCIPALS.PENNKEY_WHO_ADDED IS 'pennkey who added the record';
COMMENT ON COLUMN SERVICE_PRINCIPALS.PENNID_WHO_ADDED IS 'pennid who added the record';
CREATE UNIQUE INDEX SERVICE_PRINCIPALS_PK ON SERVICE_PRINCIPALS
(PRINCIPAL_NAME);

ALTER TABLE SERVICE_PRINCIPALS ADD (
  CONSTRAINT SERVICE_PRINCIPALS_PK
  PRIMARY KEY
  (PRINCIPAL_NAME)
  USING INDEX SERVICE_PRINCIPALS_PK);

Subject source in sources.xml

Code Block
  <!-- Service Principal Subject Resolver -->
 <source adapterClass="edu.internet2.middleware.subject.provider.JDBCSourceAdapter">
    <id>servPrinc</id>
    <name>Kerberos service principals</name>
     <type>application</type>
     <init-param>
       <param-name>jdbcConnectionProvider</param-name>
       <param-value>edu.internet2.middleware.grouper.subj.GrouperJdbcConnectionProvider</param-value>
     </init-param>
     
    <!-- on a findPage() this is the most results returned --> 
    <init-param>
      <param-name>maxPageSize</param-name>
      <param-value>100</param-value>
    </init-param>
      <init-param>
       <param-name>SubjectID_AttributeType</param-name>
       <param-value>loginid</param-value>
     </init-param>
     <init-param>
       <param-name>Name_AttributeType</param-name>
       <param-value>name</param-value>
     </init-param>
     <init-param>
       <param-name>Description_AttributeType</param-name>
       <param-value>description</param-value>
     </init-param>
     <!-- init-param>
       <param-name>maxResults</param-name>
       <param-value>1000</param-value>
     </init-param -->
     <init-param>
       <param-name>sortAttribute0</param-name>
       <param-value>loginid</param-value>
     </init-param>
     <init-param>
       <param-name>searchAttribute0</param-name>
       <param-value>loginid</param-value>
     </init-param>
      <!-- if you are going to use the inclause attribute
        on the search to make the queries batchable when searching
        by id or identifier -->
      <init-param>
        <param-name>useInClauseForIdAndIdentifier</param-name>
        <param-value>true</param-value>
      </init-param>
      
      <!-- comma separate the identifiers for this row, this is for the findByIdentifiers if using an in clause -->
      <init-param>
        <param-name>identifierAttributes</param-name>
        <param-value>loginid</param-value>
      </init-param>
     <search>
         <searchType>searchSubject</searchType>
      <param>
          <param-name>numParameters</param-name>
          <param-value>1</param-value>
        </param>
         <param>
             <param-name>sql</param-name>
             <param-value>
select
   principal_name as name,
   principal_name as loginid,
   principal_name as description
from
   service_principals
where
    {inclause}
             </param-value>
          </param>
          <param>
              <param-name>inclause</param-name>
              <param-value>
 principal_name = ?
             </param-value>
          </param>
     </search>
     <search>
         <searchType>searchSubjectByIdentifier</searchType>
      <param>
          <param-name>numParameters</param-name>
          <param-value>1</param-value>
        </param>
         <param>
             <param-name>sql</param-name>
             <param-value>
select
   principal_name as name,
   principal_name as loginid,
   principal_name as description
from
   service_principals
where
    {inclause}
             </param-value>
          </param>
          <param>
              <param-name>inclause</param-name>
              <param-value>
 principal_name = ?
             </param-value>
         </param>
     </search>
     <search>
        <searchType>search</searchType>
     <param>
          <param-name>numParameters</param-name>
          <param-value>1</param-value>
        </param>
         <param>
             <param-name>sql</param-name>
             <param-value>
select
   principal_name as name,
   principal_name as loginid,
   principal_name as description
from
   service_principals
where
   (lower(principal_name) like lower(concat('%',concat(?,'%'))))
             </param-value>
         </param>
     </search>
   </source>

Script to add a user (including to the WS group)

Code Block
grouperSession = GrouperSession.startRootSession();
kerb = "something/somewhere.institution.edu";
reason = "canvas grouper integration";
sqlRun("insert into service_principals (principal_name, id, last_updated, reason) values ('" + kerb + "', hibernate_sequence.nextval, systimestamp, '" + reason + "')");
addMember("etc:ldapUsers", kerb);
addMember("etc:webServiceClientUsers", kerb);

Run the script

Code Block
./gsh scriptName.gsh

Default authentication

Out of the box, grouper-ws uses container authentication (non-rampart).   The web.xml protects all services and expects the users to be in the role "grouper_user".  For tomcat, in the tomcat-users.xml, just have entries like this, and you are all set:

No Format
<role rolename="grouper_user"/>
  <user username="jota" password="whatever" roles="grouper_user"/>
  <user username="jobr" password="whatever" roles="grouper_user"/>
  <user username="eldo" password="whatever" roles="grouper_user"/>

Note that users to the web service need to be Subjects, and you can configure the Configure the default source in the grouper-ws.properties especially if you have subjectId overlap in various sources.

Note the default authentication in grouper-ws is http-basic, so for this and other reasons make sure your deployments of grouper-ws are protected with SSL. 

Note that, for some container technologies, container authentication can be externalized in various ways. A common deployment configuration is to externalize tomcat authentication to Apache 2.2+ using the AJP protocol. This permits several popular authentication technologies to be used in conjunction with grouper-ws.

If you do not want to use the servlet container simple auth (even if you front with apache), you need to remove the security settings at the bottom of the web.xml 

Apache in front of the servlet container can do authn and populate remote user.

A servlet filter can do custom authentication and populate remote user

HTTP basic with kerberos

Grouper-ws comes with an option to authenticate REST or SOAP with HTTP basic auth, but using the user/pass to authenticate to a kerberos kdc.  This exists so that kerberos can be used with REST (for SOAP, ws-security would be more secure), and as an example of how to customize the authentication.  You should use SSL if you use this authentication.  This defeats the purpose of kerberos since it transmits the password over the wire, and it only authenticates to the kdc and not an SSL service, so it might be possible for someone to spoof the kdc.   To use this, make the following settings in the grouper-ws.properties (obviously you need to configure the kerberos settings to fit your institution):

No Format
# to provide custom authentication (instead of the default httpServletRequest.getUserPrincipal()
# for non-Rampart authentication.  Class must implement the interface:
# edu.internet2.middleware.grouper.ws.security.WsCustomAuthentication
# class must be fully qualified.  e.g. edu.school.whatever.MyAuthenticator
# blank means use default: edu.internet2.middleware.grouper.ws.security.WsGrouperDefaultAuthentication
ws.security.non-rampart.authentication.class = edu.internet2.middleware.grouper.ws.security.WsGrouperKerberosAuthentication

################# KERBEROS settings, only needed if doing kerberos simple auth ################
# realm, whatever your realm is, e.g. SCHOOL.EDU
kerberos.realm = SCHOOL.EDU
# address of your kdc, e.g. kdc.school.edu
kerberos.kdc.address = kdc.school.edu

HTTP basic with ldap

Grouper-ws comes with an option to authenticate against ldap in 2.1.4+.  Pass the user/pass in basic auth (over SSL), and grouper can bind to an ldap server.  The username will be the subject id or identifier.

You can specify the following in the grouper-loader.properties (might already have this configured, you dont have to configure twice), note you dont have to have a user/pass since it will use the one from the authenticating WS user. Also, pooling is not applicable to this

Code Block
################################# 
## LDAP connections 
################################# 
# specify the ldap connection with user, pass, url 
# the string after "ldap." is the ID of the connection, and it should not have 
# spaces or other special chars in it. In this case is it "personLdap" 

#note the URL should start with ldap: or ldaps: if it is SSL. 
#It should contain the server and port (optional if not default), and baseDn, 
#e.g. ldaps://ldapserver.school.edu:636/dc=school,dc=edu 
#ldap.personLdap.url = ldaps://ldapserver.school.edu:636/dc=school,dc=edu 

#optional, if you are using tls, set this to true. Generally you will not be using an SSL URL to use TLS... 
#ldap.personLdap.tls = false 

#optional, if using sasl 
#ldap.personLdap.saslAuthorizationId = 
#ldap.personLdap.saslRealm = 

#optional (note, time limit is for search operations, timeout is for connection timeouts), 
#most of these default to vt-ldap defaults. times are in millis 
#validateOnCheckout defaults to true if all other validate methods are false 
#ldap.personLdap.timeout = 

Then you can specify this in the grouper-ws.properties:

Code Block
# to provide custom authentication (instead of the default httpServletRequest.getUserPrincipal() 
# for non-Rampart authentication. Class must implement the interface: 
# edu.internet2.middleware.grouper.ws.security.WsCustomAuthentication 
# class must be fully qualified. e.g. edu.school.whatever.MyAuthenticator 
# blank means use default: edu.internet2.middleware.grouper.ws.security.WsGrouperDefaultAuthentication 
# kerberos: edu.internet2.middleware.grouper.ws.security.WsGrouperKerberosAuthentication 
# ldap: edu.internet2.middleware.grouper.ws.security.WsGrouperLdapAuthentication 
ws.security.non-rampart.authentication.class = edu.internet2.middleware.grouper.ws.security.WsGrouperLdapAuthentication 

# if ldap authn should cache results 
ws.authn.ldap.cacheResults = true 

# if ldap authn should be used, which ldap connection name in the grouper-loader.properties should 
# be used for the connection to the ldap 
ws.authn.ldap.grouperLoaderLdapConfigId = personLdap 

# if ldap authn should be used, this is the prefix of the userId when connecting to ldap, e.g. uid= 
ws.authn.ldap.loginDnPrefix = 

# if ldap authn should be used, this is the suffix to the userId when connecting to ldap, e.g. ,ou=users,dc=school,dc=edu 
ws.authn.ldap.loginDnSuffix = 

Note, if you want to debug this, put this in the log4j.properties:

Code Block
log4j.logger.edu.internet2.middleware.grouper.ws.security.WsGrouperLdapAuthentication = DEBUG

Custom authentication plugin

If you want custom authentication (e.g. pass in a token, and decode it), then implement the interface edu.internet2.middleware.grouper.ws.security.WsCustomAuthentication and configure your fully qualified classname in the grouper-ws.properties.  The default is an implementation of this interface as an example: edu.internet2.middleware.grouper.ws.security.WsGrouperDefaultAuthentication, which just gets the user from the container: httpServletRequest.getUserPrincipal().getName()

Rampart 

Rampart is Jakarta's WS-Security implementation.  We have vanilla Rampart authentication working with grouper-ws (thanks to Sanjay Vivek).  Unfortunately it doesnt work out of the box since it seems Rampart and basic auth cannot work together in the web app.  If you want to run basic auth and rampart at the same time, you should deploy two separate web apps.

Note the URL for rampart in grouper-ws is the same, it will look like this: /grouper-ws/services/GrouperService

Also, for Rampart, you need custom logic to authenticate users.  To use rampart, configure the grouper-ws.properties entry: ws.security.rampart.authentication.class.  An example is: edu.internet2.middleware.grouper.ws.security.GrouperWssecSample.  Until you configure that, clients will get a 404 http status code.  This assumes you are using WSPasswordCallback, if not, just provide your own class directly to the services.xml file (and grouper-ws requires you have an implementation of the interface anyway which wont be executed). 

You need to tell grouper that wssec is enabled in the web.xml servlet param (uncomment):

No Format
<servlet>
        <servlet-name>AxisServlet</servlet-name>
        <display-name>Apache-Axis Servlet</display-name>
        <servlet-class>edu.internet2.middleware.grouper.ws.GrouperServiceAxisServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
    <!-- hint that this is the wssec servlet -->
    <init-param>
      <param-name>wssec</param-name>
      <param-value>true</param-value>
    </init-param>
    </servlet>

Also you need to comment out the container auth in web.xml:

No Format
<!--  security-constraint>
        <web-resource-collection>
            <web-resource-name>Web services</web-resource-name>
            <url-pattern>/services/*</url-pattern>
        </web-resource-collection>
        <auth-constraint>
            <role-name>grouper_user</role-name>
        </auth-constraint>
    </security-constraint -->

Then you need to enable the correct .aar file. 

  • If you are using a binary grouper-ws.war, just rename the following two files
    • /WEB-INF/services/GrouperService.aar to /WEB-INF/services/GrouperService.aar.ondeck
    • /WEB-INF/services/GrouperServiceWssec.aar.ondeck to /WEB-INF/services/GrouperServiceWssec.aar
  • If you are building, just set the param in the build.properties: webapp.authentication.use.rampart
    Here is a sample client

HTTP basic authentication (use)

 In the web.xml for the grouper-ws project, protect all services:

No Format
<security-constraint>
		<web-resource-collection>
			<web-resource-name>Web services</web-resource-name>
			<url-pattern>/services/*</url-pattern>
		</web-resource-collection>
		<auth-constraint>
			<!-- NOTE:  This role is not present in the default users file -->
			<role-name>grouper_user</role-name>
		</auth-constraint>
	</security-constraint>

	<!-- Define the Login Configuration for this Application -->
	<login-config>
		<auth-method>BASIC</auth-method>
		<realm-name>Grouper Application</realm-name>
	</login-config>

	<!-- Security roles referenced by this web application -->
	<security-role>
		<description>
			The role that is required to log in to the Manager
			Application
		</description>
		<role-name>grouper_user</role-name>
	</security-role>
</web-app>

Now send the user and pass in the web service client.
Here is an example with Axis generated clients:

No Format
GrouperServiceStub stub = new GrouperServiceStub(
                "http://localhost:8090/grouper-ws/services/GrouperService");
        Options options = stub._getServiceClient().getOptions();
        HttpTransportProperties.Authenticator auth = new HttpTransportProperties.Authenticator();
        auth.setUsername("user");
        auth.setPassword("pass");

        options.setProperty(HTTPConstants.AUTHENTICATE, auth);

Here is an example with a manual HttpClient:

, but also to help with performance

Code Block
# If there is an entry here for group name, then all web service client users must be in this group (before the actAs)
#ws.client.user.group.name = etc:webServiceClientUsers

# allow these ids even if not in group, e.g. for testing
# subjectIdOrIdentifier  or  sourceId::::subjectId  or  ::::subjectId  or  sourceId::::::subjectIdentifier  or  ::::::subjectIdentifier
# sourceId::::::::subjectIdOrIdentifier  or  ::::::::subjectIdOrIdentifier
# {valueType: "subject", multiple: true}
ws.client.user.group.subjects.allow = 

# cache the decision to allow a user to user web services, so it doesnt have to be calculated each time
# defaults to 5 minutes: 
# {valueType: "integer", required: true}
ws.client.user.group.cache.minutes = 5

# if you have subject namespace overlap (or not), set the default subject 
# sources (comma-separated) to lookup the user if none specified in user name
# {valueType: "string"}
ws.logged.in.subject.default.source = 

# prepend to the userid this value (e.g. if using local entities, might be:    etc:servicePrincipals:   )
# {valueType: "string"}
ws.security.prependToUserIdForSubjectLookup = 
No Format
HttpClient httpClient = new HttpClient();
        GetMethod getMethod = new GetMethod(
                "http://localhost:8091/grouper-ws/services/GrouperService/addMemberSimple?groupName=aStem:aGroup&subjectId=10021368&actAsSubjectId=GrouperSystem");

        httpClient.getParams().setAuthenticationPreemptive(true);
        Credentials defaultcreds = new UsernamePasswordCredentials("user", "pass");
        httpClient.getState().setCredentials(new AuthScope("localhost", 8091), defaultcreds);

        httpClient.executeMethod(getMethod);

ActAs configuration

To enable web service users to act as another user (proxy), enable the setting in the grouper-ws grouper.properties

...