Actually only RelyingParty role is managed. Some parts of this documentation is focused on that role.
Both Snapshots and Released artifacts are available on GitHub Packages:
- if you use Maven
<dependency>
<groupId>it.spid.cie.oidc</groupId>
<artifactId>it.spid.cie.oidc.starter.kit</artifactId>
<version><!--replace with the wanted version --></version>
</dependency>
- if you use Gradle
implementation group: 'it.spid.cie.oidc', name: 'it.spid.cie.oidc.starter.kit', version: 'xxx'
Unfortunately, as stated in the documentation, to use GitHub packages you have define GitHub repository in your ~/.m2/settings.xml
(or settings.gradle
) together with your credentials.
The "starter-kit" is a backend library with few dependencies:
org.json:json
, a simple and light-weigth to parse and create JSON documentscom.nimbusds:nimbus-jose-jwt
, the most popular java Library to manage JSON Web Token (JWT, JWE, JWS)com.github.stephenc.jcip:jcip-annotations:1.0-1
, a clean room implementation of the JCIP Annotationsorg.slf4j:slf4j-api
The starter-kit doesn't interact directly with the database: it delegates to the implementer this job.
It exposes an interface, called PersistenceAdapter
, and in the implementing class your application is free to use the database engine and the integration framework more suitable.
The starter-kit manages the following models:
FederationEntity
. Contains all the elements needed to describe an Entity members of the FederationCachedEntityInfo
. An EntityInfo is the set of elements (the statement) the Federation Authority has about a Federation Entity. We have to cache this information to reduce network overheadTrustChain
. Contains all the elements needed to describe the "chain of trust" between two Federation Entity.AuthnRequest
. Describe an Authentication Request and helps to track the authentication flowAuthnToken
. Contains the Authentication Token and the Claims received on the authentication flow
Every model contains these helping attributes:
createDate
modifiedDate
storageId
The SpringBoot example project uses H2 as database engine and interact with it using a SpringBoot specific data framework. In the example: * there are annotated model classes able to marshalling/unmarshalling with starter-kit models * the class implementing PersistenceAdapter interact with data framework to store the information or to find it.
Take a look to
/examples/relying-party-spring-boot/src/main/resources/sql/schema.sql
for the sample database entities structure
The class RelyingPartyOptions
contains all the defaults and the limits for a RelyingParty implementation.
Many options can be customized by your application. Some of them have to be customized because no defaults are present. An example of minimum, mandatory, configuration can be
Map<String, String> spidProviders = new HashMap<>();
spidProviders.put(<SPID_PROVIDER>, <TRUST_ANCHOR>);
RelyingPartyOptions options = new RelyingPartyOptions()
.setDefaultTrustAnchor(<TRUST_ANCHOR>)
.setClientId(<RELYING_PARTY>)
.setSPIDProviders(spidProviders)
.setTrustAnchors(ArrayUtil.asSet(<TRUST_ANCHOR>))
.setApplicationName("Sample RP")
.setRedirectUris(ArrayUtil.asSet(<RELYING_PARTY> + "callback"))
.setJWK(<RELYING_PARTY_JWK>)
.setTrustMarks(<RELYING_PARTY_TRUST_MARKS>);
where:
<TRUST_ANCHOR>
is the Trust Anchor subject (es:http.//trust-anchor.org/
)<SPID_PROVIDER>
is the SPID Provider subject (es:http://spid-provider.org/oidc/op/
)<RELYING_PARTY>
is the RelyingParty subject (es:http://my-relying-party.org/oidc/rp/
)<RELYING_PARTY_JWK>
is the string representation of a JSON WebKey (JWK), with both private and public parts, of your RelyingParty. This JWK is generated in the on-boarding flow as described below<RELYING_PARTY_TRUST_MARKS>
is the string representation of the TrustMarks generated by the Federation Authority at the end of the on-boarding flow
Your application can manage the configuration and how admin user provide it in the more suitable way. The starter kit doesn't force you in some way. You can use a configuration file (like the SpringBoot example) or a database entity or something else.
The starter-kit throws different application Exceptions but all of them are or extends OIDCException
. Also standard Exceptions are rethrows as specific OIDCExceptions.
This allows you code to be more flexible: you don't have to change method signature when a new kind of OIDCExcetion will be implemented.
The starter-kit doesn't expose web-servlets: it delegates to the implementer this job.
Your application can use preferred framework to expose the required endpoints and, internally, call RelyingPartyHandler
to process datas. The required endpoints are:
-
.well-known/openid-fedaration
. Is the only one with a fixed name and is used to expose federation entity informations to other federation entities -
authorize
. This endpoint is the starting point for OIDC SPID/CIE authentication. The webpath is customizable. Is used inside you application UI on "SignIn with SPID" and "SignIn with CIE" buttons. The request is of type GET and supports the following parameters:provider
, REQUIRED. The http url corresponding to a subject id of a SPID/CIE OIDC Provider.redirect_uri
, OPTIONAL. One of the redirect_uri available in RP's metadata.scope
, OPTIONAL. One or more of the scopes, default is openid.consent
, OPTIONAL. Sets SPID or CIE extended consent values.trust_anchor
, OPTIONAL. Sets the Trust Anchor to resolve the Federation. Default isoptions.defaultTrustAnchor
.acr_values
, OPTIONAL.profile
, OPTIONAL. Default: spid. Set (spid, cie)
-
callback
. This endpoint corresponds to the redirect uri where the auth code lands. The webpath is customizable and have to be setted inoptions.redirectUris
. This endpoint accepts a request with this parameters:code
, REQUIRED. Authorization codestate
, REQUIRED. State value enclosed in the authentication request
-
logout
. This endpoint starts a token revocation flow. The webpath is customizable.
The SpringBoot example use Spring specific approach to expose previous endpoints on `http://hostname[:port]/oidc/rp/` uri. Inside the endpoint implementation the starter-kit `RelyinPartyHandler` il called.
The RelyingPartyHandler
class is the main element of the starter-kit for the RelyingParty role.
To istantiate the class you have to provider the options and the PersistenceAdapter implementation. The correctness of these elements is checked immediately in the constructor.
RelyingPartyHandler handler = new RelyingPartyHandler(options, persistenceAdapter);
A good approach is to instantiate only one handler per application (or per tenant) and to re-instantiate it when configuration changes.
The RelyingPartyHandler
manages these following aspects:
If you istantiate RelyingPartyOptions
with no <RELYING_PARTY_JWK>
the following call
WellKnowData wellKnown = handler.getWellKnownData(true);
generates a JWON Web Key (JWK) and return it inside the WellKnowData
object as json string.
This information have to be managed by your application (stored somewhere) to be reused to re-instantiate RelyingPartyOptions
and RelyingPartyHandler
.
For the on-boarding process you have to provide to the Federation Authority only the public part of the JWK (wellKnown.getPublicJwks()
).
If you istantiate RelyingPartyOptions
with <RELYING_PARTY_JWK>
and no <RELYING_PARTY_TRUST_MARKS>
your RelyingParty is in an intermediate on-boarding status.
The starter-kit exposes two methods
public WellKnownData getWellKnownData(boolean jsonMode) throws OIDCException;
public WellKnownData getWellKnownData(String requestURL, boolean jsonMode) throws OIDCException;
The second one is more suitable to be used into .well-known/openid-federation
endpoint implementation because extracts entity's subject-id from the requested URL and guarantee your application is calling the right handler object instance.
When both <RELYING_PARTY_JWK>
and <RELYING_PARTY_TRUST_MARKS>
are specified into RelyingPartyOptions
any call to wellKnownData()
methods provides a the final federation entity configuration.
This final information is also stored, via PersistenceAdapter, into your application database and retrieved, via PersistenceAdapter, to avoid rebuilding effort.
The first step of the authentication flow is to redirect the user to the OIDC Provider sign in page to authenticate itself, and sent back to the provided callback uri.
In your application UI the "SignIn" button invoke your implementation of authorize
endpoint. This implementation call the handler with the provided params
String authURL = relyingPartyHandler.getAuthorizeURL(
oidcProvider, trustAnchor, redirectUri, scope, profile, consent);
than the handler:
- validate the provider arguments against provided options
- fetch, via PersistenceAdapter, the TrustChain of the provider
- use it if still valid
- otherwise fetch information from the Provider and the TrustAnchor to build a TrustChain
- store the new TrustChain (via PersistenceAdapter)
- create and store, via PersistenceAdapter, an AuthnRequest object
- build the url you application have to use to redirect the browser to the Provider for authentication
If something goes wrong in this process an OIDCException is thrown; otherwise your application receive the URL to redirect the browser.
On the second step of the authentication flow the control come back to your application to the callback
endpoint.
Your application have to forward to RelyingPartyHandler
only when sign-in happens successfully; errors have to be managed directly by your code as expected by the used framework. For "not successfully" we mean all the situations when the OIDC Provider reply doesn't contains the required params "code" and "scope".
JSONObject userInfo = relyingPartyHandler.getUserInfo(state, code);
The handler:
- fetch the AuthnRequest, via PersistenceAdapter, matching provided params
- obtain, from the OIDC Provider, a valid access token
- obtain, from the OIDC Provider, the configured Claims (into RelyingPartyOptions)
- store, via PersistenceAdapter, the AuthnToken with the claims
- return a JSONObject with only the Claims
The AuthnToken model contains the "userKey" field filled from the value of the configured Claim (options.getUserKeyClaim()
). This field is extremely important because is needed in the logout process.
The JSONObject looks like
{
sub: "e6b06083c2644bdc06f5a1cea22e6538b8fd59fc091837938c5969a8390be944",
"given_name": "peppe",
"family_name": "maradona",
"email": "that@ema.il",
"https://attributes.eid.gov.it/fiscal_number": "8sada89s7da89sd7a98sd78",
}
If something goes wrong in this process an OIDCException is thrown; otherwise your application can process the claims for its the business needs, for example: - create or update the User profile inside the application database - redirect the user to the right landing page
Your application is the real owner of the logger user profile information. Is responsability of your application to merge it when user sign in with different providers.
The revocation flow is started by the user from your application UI. Your implementation of the logout
endpoint have to call the handler with two elements:
- the userKey of the user session
- a class implementing
RelyingPartyLogoutCallback
String redirectURL = relyingPartyHandler.performLogout(userKey, callback);
The handler:
- fetch the AuthnRequest and AuthnToken, via PersistenceAdapter, for the user session
- fire the callback to allow your application to invalidate the user session
- send a revocation request to the OIDC Provider
- return the logout landing url
If something goes wrong in this process an OIDCException is thrown; otherwise your application will receive the URL and can perform the redirect.
Your application have to provide the user two buttons: "SignIn with SPID" and "SignIn with CIE". Both buttons present a drop-down list with configured OIDC Providers randomly sorted.
While the known providers are setted inside RelyingPartyOptions
(setSPIDProviders()
and setCIEProviders()
), the RelyingPartyHandler
provide a method to obtain the information needed to fill the drop-down list.
List<ProviderButtonInfo> infos = relyingPartyHandler.getProviderButtonInfos(OIDCProfile.SPID)
Every ProviderButtonInfo
contains:
- subject id of the provider
- organization name
- logo url
To provide these information the handler use the TrustChain model it has of the provider. It could be the one already stored in the database, via PersistenceAdapter, or the one builded on the fly (if the stored one is absent or invalid) and than stored.