OpenConext implementation of a OpenID Connect server based on the MITREid Connect server
- Java 7
- Maven 3
- MySQL 5.5+
Connect to your local mysql database: mysql -uroot
Execute the following to create a local datbase compliant with travis:
CREATE DATABASE `oidcserver` DEFAULT CHARACTER SET latin1;
create user 'root'@'localhost';
grant all on `oidcserver`.* to 'root'@'localhost';
The OpenConext-oidc is a maven overlay for OpenID-Connect-Java-Server. Issue a
git submodule update --init --recursive
command to set up the git submodules, then you can run
cd ./oidc-server && mvn clean package jetty:run -Dspring.profiles.active="local"
or the shorthand:
./start.sh
If you don't use the local profile then you need to login on the SURFconext federation.
There are JUnit integration tests that will run against the locally started Jetty container.
mvn verify
You can also start the OIDC server (local mode !) and then run the tests from within your IDE (e.g. you can debug either the test or the OIDC server).
The integration tests set the spring.active.profile property automatically to local too prevent having to do the SAML dance.
When you have the oidc server running locally with the local profile you can use cUrl to test the different endpoints.
Note that this only works because of the local
profile where there is pre-authenticated user provided by the MockPreAuthenticatedProcessingFilter
.
First obtain an authorization code:
curl -i "http://localhost:8080/authorize?response_type=code&client_id=https@//oidc.localhost.surfconext.nl&scope=openid&redirect_uri=http://localhost:8889/callback"
This will output the following:
HTTP/1.1 302 Found
Date: Mon, 07 Mar 2016 14:39:33 GMT
Set-Cookie: JSESSIONID=15hhbacbubrc73iy6ioqj01rx;Path=/
Expires: Thu, 01 Jan 1970 00:00:00 GMT
X-Frame-Options: DENY
Pragma: no-cache
Cache-Control: no-cache
Cache-Control: no-store
Content-Language: en
Location: http://localhost:8889/callback?code=s1JRqh
Content-Length: 0
Server: Jetty(9.3.5.v20151012)
Save the code in the query parameter of the location response header in a variable (use the code of your response and not this example code):
export code=s1JRqh
And then exchange the code for an access token:
curl -X POST -u https@//oidc.localhost.surfconext.nl:secret -d "grant_type=authorization_code&code=${code}&redirect_uri=http://localhost:8889/callback" http://localhost:8080/token | python -m json.tool
This will return the access_token and id_token.
{
"access_token": "eyJraWQiOiJvaWRjIiwiYWxnIjoiUlMyNTYifQ.eyJhdWQiOiJodHRwc0BcL1wvb2lkYy5sb2NhbGhvc3Quc3VyZmNvbmV4dC5ubCIsImlzcyI6Imh0dHA6XC9cL2xvY2FsaG9zdDo4MDgwXC8iLCJleHAiOjE0NTc1MDgxMzAsImlhdCI6MTQ1NzQyMTczMCwianRpIjoiNTI3MTI0OGEtMGY3ZC00ODY3LTllZTUtMDgwMzMyYTYwOWZmIn0.SqdfoVVYIL-EXI0hmTRQzWCrtYL5rXz_Sgxvg0SfI3nn68dxCjxV9r00inJqXCm6lkD3uKdSfzpQ2EfGLhHCqpKZxNGQoDvEIghqrqZPGOxMu_vbfiKudCR8gxag_xIm-kkiLMkd_5iBFK3QlCmVHoJjnfxZkXYb3-bMKdYA1ourP4U4pvFWxHIcnk20QQO-NIawt3brU3nryErtGdEDepultN26qdgTubvmAQRaRF0OYyia1eOTVYwEKdted6E8INqRmR5WFWJIg_7HqE4c9JcHOMd8PCv558N0QU3G49Oqpn7xlBN7fvZq0RpCsGcTJAkqLjqRi-a0VHsvN7hJvw",
"expires_in": 86399,
"id_token": "eyJraWQiOiJvaWRjIiwiYWxnIjoiUlMyNTYifQ.eyJzdWIiOiI3NTcyNmUzYS02MzZmLTZjNmMtNjE2Mi0zYTcwNjU3MjczNmYiLCJhdWQiOiJodHRwc0BcL1wvb2lkYy5sb2NhbGhvc3Quc3VyZmNvbmV4dC5ubCIsImtpZCI6Im9pZGMiLCJpc3MiOiJodHRwOlwvXC9sb2NhbGhvc3Q6ODA4MFwvIiwiZXhwIjoxNDU3NTA4MTMwLCJpYXQiOjE0NTc0MjE3MzAsImp0aSI6IjI1YjJhZmFmLWEwZWYtNGVjOS05NzQ1LTE1YWQyYjUyMWVmNiJ9.kMM8EjkvgvGgYlzOSJL11Qqzoq8M0av1HaFs7tnMO4E7kPsoT25WzXwreaW3GRk56C9HyHKtFVo7f836_yNocWFkARliSUv43onVV6ro7BL41EFROWmJBR2iBMmKH_Pn8SXO-kYvWg0r5S3zlpaiWL_xqgW6yoOe32vlcQbhteixT3OwVDMNe6XuVlOU2K7XtJmsZQml5py0mLVOyi068ag7uIJ1lA9mLkcw86i8Edzye2Wdxx1_DNF4D_d7MhRjJi5IxGdcADmNeAI8-iYS12v0joctCDKSFff8jee5OFlhC5DFZ6EzhoVIYuY0dLbgHFztG2Q3ScGFIq9BNKZHFw",
"scope": "openid",
"token_type": "Bearer"
}
Save the access_token in a variable:
export access_token=eyJraWQiOiJvaWRjIiwiYWxnIjoiUlMyNTYifQ.eyJhdWQiOiJodHRwc0BcL1wvb2lkYy5sb2NhbGhvc3Quc3VyZmNvbmV4dC5ubCIsImlzcyI6Imh0dHA6XC9cL2xvY2FsaG9zdDo4MDgwXC8iLCJleHAiOjE0NTc1MDgxMzAsImlhdCI6MTQ1NzQyMTczMCwianRpIjoiNTI3MTI0OGEtMGY3ZC00ODY3LTllZTUtMDgwMzMyYTYwOWZmIn0.SqdfoVVYIL-EXI0hmTRQzWCrtYL5rXz_Sgxvg0SfI3nn68dxCjxV9r00inJqXCm6lkD3uKdSfzpQ2EfGLhHCqpKZxNGQoDvEIghqrqZPGOxMu_vbfiKudCR8gxag_xIm-kkiLMkd_5iBFK3QlCmVHoJjnfxZkXYb3-bMKdYA1ourP4U4pvFWxHIcnk20QQO-NIawt3brU3nryErtGdEDepultN26qdgTubvmAQRaRF0OYyia1eOTVYwEKdted6E8INqRmR5WFWJIg_7HqE4c9JcHOMd8PCv558N0QU3G49Oqpn7xlBN7fvZq0RpCsGcTJAkqLjqRi-a0VHsvN7hJvw
Now you can ask the server to return the information stored with this access_token by calling the introspect endpoint (note that this endpoint is only for resource servers):
curl -u https@//oidc.localhost.surfconext.nl:secret -H "Content-Type: application/x-www-form-urlencoded" "http://localhost:8080/introspect?token=${access_token}" | python -m json.tool
This will return:
{
"active": true,
"authenticating_authority": "http://mock-idp",
"client_id": "https@//oidc.localhost.surfconext.nl",
"edu_person_principal_name": "principal_name",
"exp": 1457508131,
"expires_at": "2016-03-09T08:22:11+0100",
"schac_home": "surfnet.nl",
"scope": "openid",
"sub": "75726e3a-636f-6c6c-6162-3a706572736f",
"token_type": "Bearer",
"unspecified_id": "urn:collab:person:example.com:local",
"user_id": "75726e3a-636f-6c6c-6162-3a706572736f"
}
Use the same access_token to call the user_info endpoint:
curl -H "Authorization: bearer ${access_token}" -H "Content-type: application/json" http://localhost:8080/userinfo | python -m json.tool
This will return all the information about the user. This endpoint is for ServiceProviders.
{
"edu_person_affiliations": [
"student",
"faculty"
],
"edu_person_entitlements": [
"http://xstor.com/contracts/HEd123",
"urn:mace:washington.edu:confocalMicroscope"
],
"edu_person_principal_name": "principal_name",
"edu_person_scoped_affiliations": [
"student",
"faculty"
],
"edu_person_targeted_id": "fd9021b35ce0e2bb4fc28d1781e6cbb9eb720fed",
"email": "john.doe@example.org",
"family_name": "Doe",
"given_name": "John",
"is_member_ofs": [
"surfnet"
],
"locale": "NL",
"name": "John Doe",
"preferred_username": "John Doe",
"schac_home_organization": "surfnet.nl",
"schac_home_organization_type": "institution",
"schac_personal_unique_codes": [
"personal"
],
"sub": "75726e3a-636f-6c6c-6162-3a706572736f",
"uids": [
"uid2",
"uid1"
]
}
The OIDC application uses a JWK Key Set to sign and optionally encrypt the JSON Web Tokens (JWT). Each environment can have its own unique
JWK Key Set. In the ansible projects the oidc_server_oidc_keystore_jwks_json secret
is used to set populate the file oidc.keystore.jwks.json
with the key information. If you need a new JWK Key Set run OidcKeystoreGenerator:
cd oidc-server ; mvn compile ; mvn exec:java -Dexec.mainClass="oidc.security.OidcKeystoreGenerator" -Dexec.classpathScope=runtime
The SAML Spring Security library needs a private DSA key to sign the SAML request and the public certificates from EngineBlock. The public certificate can be copied from the metadata. The private / public key for the SP can be generated:
openssl req -subj '/O=Organization, CN=OIDC/' -newkey rsa:2048 -new -x509 -days 3652 -nodes -out oidc.crt -keyout oidc.pem
The Java KeyStore expects a pkcs8 DER format for RSA private keys so we have to re-format that key:
openssl pkcs8 -nocrypt -in oidc.pem -topk8 -out oidc.der
Remove the whitespace, heading and footer from the oidc.crt and oidc.der:
cat oidc.der |head -n -1 |tail -n +2 | tr -d '\n'; echo
cat oidc.crt |head -n -1 |tail -n +2 | tr -d '\n'; echo
Above commands work on linux distributions. On mac you can issue the same command with ghead
after you install coreutils
:
brew install coreutils
cat oidc.der |ghead -n -1 |tail -n +2 | tr -d '\n'; echo
cat oidc.crt |ghead -n -1 |tail -n +2 | tr -d '\n'; echo
Add the oidc key pair to the application.oidc.properties file:
sp.private.key=${output from cleaning the der file}
sp.public.certificate=${output from cleaning the crt file}
Add the EB certificate to the application.oidc.properties file:
idp.public.certificate=${copy & paste from the metadata}
OpenConext-OIDC is a proxy for SP's that want to use OpenConnect ID instead of SAML to provide their Service to the federation members. Therefore the WAYF and ARP must be scoped for the requesting SP (and not this OIDC SP). This works if OpenConext-OIDC is marked as a trusted proxy in SR and the signing certificate (e.g. sp.public.certificate) is added to the certData metadata field in SR.
We link SPs and OIDC Clients by the SP entity-id and the client name. The authorization server MUST support the HTTP Basic authentication scheme for authenticating clients that were issued a client password and Basic authentication does not support ':' in the username. We therefore substitute each "@" in the SP entity-id with a "@@" and each ":" with a "@". The algorithm needs to be revertible, because the SP entity is 'saved' during SAML request in the relay state of the AuthN request.
See ServiceProviderTranslationService
You can also query the translate endpoint:
http://oidc.${env}.surfconext.nl/translate-sp-entity-id?spEntityId=${urlEncoded SP entity ID}
Example for localhost
http://localhost:8080/translate-sp-entity-id?spEntityId=https%3A//oidc.test.surfconext.nl
The metadata is generated on the fly and is displayed on http://localhost:8080/saml/metadata
The OpenID Connect specification defines two Subject Identifier types. The OICD server is implemented to always use the pairwise type. For each client the 'sub' of the user is a unique combination of the client_id and user_id.
The openid-configuration states we support both, but that is the default (hard-coded) behaviour we inherited from the initial codebase.
Besides the 'normal' 3rd party libraries defined in the pom.xml, we also include two forked dependencies in the target war:
-
- Branch based on pull-request to support other response types then code and token.
-
- Branch based on pull-request to include RequesterID.
Once the pull requests are accepted and merged into a release we can depend on the original repositories again.
Using the authz-playground you can test all implemented flows and endpoints of the OIDC server. If you want to test the (re)provisioning of the user it is sometimes useful to add/change the default attributes that Mujina-idp returns. These extra attributes show up in the the userinfo page - step 3 in the authz-playground wizard. To add an attribute use the Mujina API:
curl -v -H "Accept: application/json" -H "Content-type: application/json" -d '{"value": ["teacher","professor"]}' -X PUT https://mujina-idp.test.surfconext.nl/api/attributes/urn:mace:dir:attribute-def:eduPersonScopedAffiliation
To reset Mujina back to its default behaviour, issue:
curl -v -H "Accept: application/json" -H "Content-type: application/json" -X POST https://mujina-idp.test.surfconext.nl/api/reset