OpenId-Connect SSO
Primer
OpenId-Connect is built upon the oauth2 specification to support sso authentication.
Terminology and Abbr
- https://{{shoutcmssitename}}.shoutcms.net can be tagged as [shoutcms]
- {{clientapp}} can be tagged as [client]
- OpenId-Connect can be abbreviated as oidc
- sso: single sign on
OpenId-Connect Overview
@see: https://openid.net/connect/ official site defining the OpenId-Connect specification
@see: https://jwt.io/ useful site for working with jwt tokens
@see: https://guides.shoutcms.net/openid-connect-sso [this page]
@see: oauth2 slideshow
OpenId-Connect ShoutCMS Configuration
@see: Shoutcms OpenId-Connect Administrator Guide to configure an OpenId-Connect application
openid-connect flow
- [shoutcms] user logs into shoutcms
- [shoutcms] clicks on the client link in their member area
- [client] {{clientapp}} checks if the user is already logged into their system... if so... done.
- [client] otherwise {{clientapp}} generates an authorization request url and redirects user back to {{shoutcmsitename}}.shoutcms.net via Location header
(Optional) you add a prompt param to the get request which will change the behaviour of the login flow (see appendix i for prompt values).redirect the user to Location: https://{{url}}/_/oauth/authorize?response_type=code&nonce={{nonce}}&client_id={{client_id}}&state={{state}}&scope={{scope}}&redirect_uri={{redirect_url}}
▶Request Vars
{{url}} = https://{{shoutcmssitename}}.shoutcms.net (during dev) once the site goes live it will be https://{{liveshoutcmssitedomainname}}
{{nonce}} = random string which should be stored and checked in the jwt tokens returned by the shoutcms server in the token step
{{client_id}} = oidc client_id
{{state}} = random string which should be stored and checked in the response from the shoutcms server
{{scope}}= openid company_name (and optionally profile, email, phone, address)
{{redirect_url}} = the url in the client application which will receive the response▶Response Body
since this is a redirect to a url inside the users browser there is technically no response
- [shoutcms] if user is not logged into into https://{{shoutcmssitename}}.shoutcms.net they need to login (since most of the time they will be already logged into shoutcms and clicking on a link to {{clientapp}} this step will usually be skipped)... but if the user stores a bookmark to the {{clientapp}} then this can happen
- [shoutcms] user approves giving information to {{clientapp}} (If you want to auto approve and allow contacts to skip this step since the app you are connecting to is trusted you can contact ShoutCMS customer care and they can help you.)
- [shoutcms] the user will be redirected back to {{clientapp}} via the redirect url
- [client] receive callback at redirect_url additional query params will be appended to the url
code = {{code}}
state = {{state}}
- get code and state vars from query string
- verify that the state matches the state which you set and stored in step where you redirect users browser to {{shoutcmsitename}}.shoutcms.net
- [client] make a token request with the {{code}} from authorization request response in the serverside of your app
POST: https://{{url}}/_/oauth
▶Request Headers
{{standard_request_headers}}
▶Request Body
grant_type: "authorization_code"
code: "{{code}}"
redirect_uri: "{{redirect_url}}"▶Request Vars
{{url}} = https://{{shoutcmssitename}}.shoutcms.net (during dev) once the site goes live it will be https://{{liveshoutcmssitedomainname}}
{{code}} = code is the value from the prior authorization request
{{redirect_url}} = the url in the client application which will receive the response
{{client_id}}=the oidc client id
{{client_secret}}=the oidc client secret▶Response Body
{ "access_token":"{{access_token}}", "expires_in":3600, "token_type":"Bearer", "scope":"openid company_name", "id_token":"{{id_token.jwt}}" }▶Response Vars
access_token = this is the api access token which can be used to query the userinfo api endpoint
id_token = this is a jwt token which includes encoded information about the user logged into https://{{shoutcmssitename}}.shoutcms.net
scope = the oidc scope requested in step 1expires_in = how long the access_token will live before requiring a new authorization request
- [client] parse response and validate response
- response will have an id_token param. The id_token is a jwt token which can be parsed into more data. a jwt is made of three parts. part 1 is the method used to encode the jwt. part 2 is the payload. and part 3 is a signature of part 2 signed with a private key in shoutcms. the public key can be used to validate that part 2 was created by shoutcms. see https://jwt.io for a useful parser and lots of documentation
- validate the payload from part 2 using the signature from part 3 and the public key found at the jwks_uri
- the id_token jwt payload will contain the nonce specified in the authorization step which should be validated against the nonce you have stored. this prevents replay attacks
- finally the id_token jwt payload includes additional useful claims
- iss: who issued the id_token this will be http://{{shoutcmssitename}}.shoutcms.net
- sub: this is the unique user id which can be used to identify the logged in user
- aud: the client_id
- iat: unixtime when the id_token was issued
- exp: unixtime when the id_token expires
- auth_time: unixtime when the user logged in (this might be different to iat if user was already logged in when the token was requested)
- the id_token now includes the userclaims so using the userinfo_endpoint should be optional
- [client] (Optional) make a request in the serverside to userinfo_endpoint to get the company name (depending on what scope you set in step 1 you will get different user claims using the access_token that you got in step 2.
POST https://{{url}}/_/oauth/userinfo▶Request Headers
{{standard_request_headers}}
Authorization: Bearer {{access_token}}▶Request Varsaccess_token = the access token returned in step 2▶Response Body{ "name": "Tester, Joe", "family_name": "Tester", "given_name": "Joe", "preferred_username": "openiduser", "profile": "", "picture": "", "website": "", "updated_at": "2022-04-26T13:41:25", "email": "test+openiduser@example.com", "address": { "formatted": "", "street_address": "", "locality": "", "region": "", "postal_code": "", "country": "" }, "phone_number": "", "https:\/\/oidc.shoutcms.net\/company_name": "Test Devel0 Mediashaker", "sub": "{{sub}}" }▶Response Vars
sub: the user identifier in shoutcms which can be used to link the shoutcms user to the {{clientapp}} user. - [client] revoke (Optional) - if user logs out in {{clientapp}} the access token can be revoked
POST https://{{url}}/_/oauth/revoke▶Request Headers
{{standard_request_headers}}
Authorization: Bearer {{access_token}}▶Request Body{
"token_type_hint": "access_token",
"token": "{{access_token}}"
}▶Request Varsaccess_token = the access token returned in step 2▶Response Body{ "revoked": true }
shoutcms expects all request to include the following standard_request_headers these are usually included by most browsers, but not necessarily by js code libraries.
Content-Type: ...
User-Agent: ...
Accept: */*
Recommendations on implementation
A good oauth2/oidc library really should handle 85% of the flow.
For example with league/oauth2-client all that is needed is to set is the client_id, client_secret, redirect_url, and the public signing key. Furthermore with web-token/jwt-framework it is possible to use the json file located at the shoutcms.net oidc_discovery_url to autoconfigure the league/oauth2-client library. code is required to generate and store the nonce and state. And then a route to handle the redirect-url callback (get/verify jwt token and optionally get userinfo request). Finally, something needs to be done with the responses/payloads.
eg. php oauth client libraries
eg. php jose libraries for working with jwt tokens, oidc,
- spomky-labs/jose legacy php library
- web-token/jwt-framework
Security
make sure you use both generate both a random nonce and random state; store them; and then verify that they match in the results from shoutcms to protect against response hacking.
Development OpenId-Connect configuration:
| client-id | **** (generated in the shoutcms admin console) |
| client-secret | **** (generated in the shoutcms admin console) |
| redirect-url | {{client_app_url}}/_/oauth/receivecode (note: this can be any url and is configured in the shoutcms admin console) |
| oidc discovery uri | https://{{shoutcmssitename}}.shoutcms.net/.well-known/openid-configuration |
| jwks_uri | https://{{shoutcmssitename}}.shoutcms.net/.well-known/openid-configuration/jwks.json |
| public signing key : PEM | **** (generated in the shoutcms admin console) eg. -----BEGIN PUBLIC KEY----- AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAA -----END PUBLIC KEY----- |
| authorization_endpoint | https://{{shoutcmssitename}}.shoutcms.net/_/oauth/authorize |
| token_endpoint | https://{{shoutcmssitename}}.shoutcms.net/_/oauth |
| userinfo_endpoint | https://{{shoutcmssitename}}.shoutcms.net/_/oauth/userinfo |
| grant-types | authorization_code |
| scope | openid company_name phone address profile email |
| state | this is a value that client can set and check matches in the responses to make sure the response is valid |
Appendix
Appendix i - prompt
Space delimited, case sensitive list of ASCII string values that specifies whether the Authorization Server prompts the End-User for reauthentication and consent. The defined values are:
none The Authorization Server MUST NOT display any authentication or consent user interface pages. An error is returned if an End-User is not already authenticated or the Client does not have pre-configured consent for the requested Claims or does not fulfill other conditions for processing the request. The error code will typically be login_required, interaction_required, or another code defined in Section 3.1.2.6. This can be used as a method to check for existing authentication and/or consent.
login The Authorization Server SHOULD prompt the End-User for reauthentication. If it cannot reauthenticate the End-User, it MUST return an error, typically login_required.
consent The Authorization Server SHOULD prompt the End-User for consent before returning information to the Client. If it cannot obtain consent, it MUST return an error, typically consent_required.
The prompt parameter can be used by the Client to make sure that the End-User is still present for the current session or to bring attention to the request. If this parameter contains none with any other value, an error is returned.
Notes:
- there is currently no role based mapping capability of admin users or standard users.
