Do you use or administer OpenEMR? Take the General Satisfaction Survey to help improve the product

V6 Authorization and API changes afoot

Currently in v6.0.0 master: Authorization Server by sjpadgett · Pull Request #4013 · openemr/openemr · GitHub
Here is a summary from post on issue thread: Implement OA2 Authorization server to support: · Issue #3956 · openemr/openemr · GitHub to catch up.
As soon as I can get some documentation done I plan to merge.

  • If you are currently or plan to use our apis, I strongly encourage you to pay attention to this PR as our api base url has to change to support multi site.
  • Also changing is one will have to register their client regardless if using the resource(password) or authorization grant as explained above. Once registered then always registered.
  • Mostly OIDC compliant authorization with some, unnecessary for openemr, items left to do.
  • Password grant can receive a token via hitting the oauth2/default/token endpoint with the password grant type.(we’ve removed the custom oauth2/default/password endpoint 11-18-2020)
  • Refresh token is as normal with new access token issued by hitting oauth2/default/token endpoint.

I plan to do a complete api session for final testing before merge by documenting vis several posts in sequence here, starting with registration.
For clarity, all example urls with ‘default’ part is the targeted site id. I’m also not going to explain what every attribute means as there is more than enough information on the web. Plus, I hate to type.:slight_smile:
Here’s registration Request(note: post_logout_redirect_uris is optional):

curl -X POST -k -H 'Content-Type: application/json' -i https://localhost/openemrv6/oauth2/default/registration --data '{
   "application_type": "private",
   "redirect_uris":
     ["https://client.example.org/callback"],
   "post_logout_redirect_uris":
     ["https://client.example.org/logout/callback"],
   "client_name": "A Private App",
   "token_endpoint_auth_method": "client_secret_post",
   "contacts": ["me@example.org", "them@example.org"]
  }'

Response:

{
    "client_id": "LnjqojEEjFYe5j2Jp9m9UnmuxOnMg4VodEJj3yE8_OA",
    "client_secret": "j21ecvLmFi9HPc_Hv0t7Ptmf1pVcZQLtHjIdU7U9tkS9WAjFJwVMav0G8ogTJ62q4BATovC7BQ19Qagc4x9BBg",
    "registration_access_token": "uiDSXx2GNSvYy5n8eW50aGrJz0HjaGpUdrGf07Agv_Q",
    "registration_client_uri": "https:\/\/localhost\/openemrv6\/oauth2\/default\/client\/6eUVG0-qK2dYiwfYdECKIw",
    "client_id_issued_at": 1604767861,
    "client_secret_expires_at": 0,
    "contacts": ["me@example.org", "them@example.org"],
    "application_type": "private",
    "client_name": "A Private App",
    "redirect_uris": ["https:\/\/client.example.org\/callback"],
    "token_endpoint_auth_method": "client_secret_post"
}

Next will be an authorization code or code grant type request:

curl -X GET -k -i 'https://localhost/openemrv6/oauth2/default/authorize?
response_type=code
&client_id=LnjqojEEjFYe5j2Jp9m9UnmuxOnMg4VodEJj3yE8_OA
&state=a85b870548dd8880ddb7c3192439f468fe63396f
&scope=openid email phone address api:pofh api:fhir
&redirect_uri=https://client.example.org/callback'

Response is a redirect to get user consent.

  1. Status : 301 Moved Permanently
  2. Location : https://localhost/openemrv6/oauth2/default/provider/login

    Ignore Register button as forgot to remove. Then

Where an authorization code is returned and client sends a request for access_token:

curl -X POST -k -H 'Content-Type: application/x-www-form-urlencoded' 
-i https://localhost/openemrv6/oauth2/default/token 
--data 'grant_type=authorization_code
&client_id=LnjqojEEjFYe5j2Jp9m9UnmuxOnMg4VodEJj3yE8_OA
&client_secret=j21ecvLmFi9HPc_Hv0t7Ptmf1pVcZQLtHjIdU7U9tkS9WAjFJwV....
&redirect_uri=https://client.example.org/callback
&code=def50200102853212bca0b139843....'

with a json response:

{
  "id_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJ...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJhdWQi...",
  "refresh_token": "def502001d015749fa13456a9d39faeccbf659..."
}

I’ll maybe come back to this to continue with refresh however, the web can describe this flow better than me. Next will do an api call using our issued token.

Here is a create patent FHIR request just to show it:

curl -X POST -k -H 'Authorization: Bearer eyJ0eXAiOiJ...' -i https://localhost/openemrv6/apis/default/fhir/Patient 
--data '{"id":"46","meta":{"versionId":"1","lastUpdated":"2020-03-24T19:49:24"},"resourceType":"Patient","active":true,"name":[{"use":"official","family":"Aaaa Jr.","given":["Jim","Adam"]}],"gender":"male","birthDate":"1969-04-11","address":[{"line":["16129 Barry Rd"],"city":"Brandon","state":"FL","postalCode":"33511"}]}'

Response is a 201 created status and body:

{
  "validationErrors": [],
  "internalErrors": [],
  "data": {
    "pid": 511,
    "uuid": "91f45c9b-36bc-4c8c-a646-030a758b771e"
  }
}

So a couple other points real quick:
Hitting the https://localhost/openemrv6/oauth2/default/.well-known/openid-configuration endpoint will return any needed discovery of supported claims and scopes and supported auth endpoints as a json.
example:

{
"issuer": "https://localhost/openemrv6/oauth2/default",
"authorization_endpoint": "https://localhost/openemrv6/oauth2/default/authorize",
"token_endpoint": "https://localhost/openemrv6/oauth2/default/token",
"jwks_uri": "https://localhost/openemrv6/oauth2/default/jwk",
"userinfo_endpoint": "https://localhost/openemrv6/oauth2/default/userinfo",
"registration_endpoint": "https://localhost/openemrv6/oauth2/default/registration",
"scopes_supported": [
    "openid",
    "profile",
    "name",
    "given_name",
    "family_name",
    "nickname",
    "phone",
    "address",
    "email",
    "email_verified",
    "api:oemr",
    "api:fhir",
    "api:port",
    "api:pofh"
],
"response_types_supported": [
    "code",
    "token",
    "id_token",
    "code token",
    "code id_token",
    "token id_token",
    "code token id_token"
],
"code_challenge_methods_supported": [
    "S256",
    "plain"
],
"grant_types_supported": [
    "authorization_code",
    "password",
    "refresh_token"
],
"response_modes_supported": [
    "query",
    "fragment",
    "form_post"
],
"subject_types_supported": [
    "public"
],
"claims_supported": [
    "aud",
    "email",
    "email_verified",
    "exp",
    "family_name",
    "given_name",
    "iat",
    "iss",
    "locale",
    "name",
    "sub"
],
"require_request_uri_registration": ["false"],
"id_token_signing_alg_values_supported": [
    "RS256"
],
"token_endpoint_auth_methods_supported": [
    "client_secret_post"
],
"token_endpoint_auth_signing_alg_values_supported": [
    "RS256"
]
}

Forgot second point.:slight_smile:

So once in codebase this phase 1 and 2(OAuth2 and OIDC) towards our ONC Api requirement. Besides Api testing requirements, the last phase of Api access will be the Smart on FHIR requirement.

Good luck.

2 Likes

Hi @sjpadgett

Really huge and great work!
I’m testing the new authentication now, meanwhile I’ve found 2 bugs with the length of the client_id and client_secret.

  1. the column length of client_id is 40 but the generated sting is 43.
  2. the column length of client_secret is 80 but the generated sting is 86.

And a few questions-

  1. What need to set in the “state” property in the GET oauth2/default/authorize call?
  2. About “refresh token”, you wrote on issue in Github “Refresh token is as normal with new access token issued by hitting oauth2/default/token endpoint.”, how it supposed to work? for new access_token need “code” and the expiration od the code is very short, can you send example of refresh, or we need new development for refresh token interface if needed, the expiration time of the access token now is 1 hour…
  3. is there interface to logout and remove access token from the system?

Thanks
Amiel

Hi @Amiel
Yep Brady caught that too. I used an old table structure when did PR but Brady did a fix straight in master. He also buttoned down our encrypt keys here: secured the mechanism for oauth key pair and encryption key by bradymiller · Pull Request #4022 · openemr/openemr · GitHub

Here’s a refresh example:

curl -X POST -k -H 'Content-Type: application/x-www-form-urlencoded' 
-i 'https://localhost/openemrv6/oauth2/default/token' 
--data 'grant_type=refresh_token
&client_id=kbyuFDidLLm280LIwVFiazOqjO3ty8KH
&client_secret=60Op4HFM0I8ajz0WdiStAbziZ-VFQttXuxixHHs2R7r7-CW8GR79l-mmLqMhc-Sa
&refresh_token=def50200a50b23cf663e51dd535f93.....'

Response is new access/id etc

{
  "id_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJhdWQiOiJrYn...",
  "token_type": "Bearer",
  "expires_in": 3599,
  "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJhdWQiOiJrYnl1RkRp...",
  "refresh_token": "def5020017b484b0add020bf3491a8a537fa04eda12..."
}

I haven’t done the logout or revoke yet but plan to in day or two.

1 Like

Sorry @Amiel forgot state attribute. Here is what I support from OIDC spec:
OpenID Connect uses the following OAuth 2.0 request parameters with the Authorization Code Flow:

scope

REQUIRED. OpenID Connect requests MUST contain the openid scope value. If the openid scope value is not present, the behavior is entirely unspecified. Other scope values MAY be present. Scope values used that are not understood by an implementation SHOULD be ignored. See Sections 5.4 and 11 for additional scope values defined by this specification.

response_type

REQUIRED. OAuth 2.0 Response Type value that determines the authorization processing flow to be used, including what parameters are returned from the endpoints used. When using the Authorization Code Flow, this value is code.

client_id

REQUIRED. OAuth 2.0 Client Identifier valid at the Authorization Server.

redirect_uri

REQUIRED. Redirection URI to which the response will be sent. This URI MUST exactly match one of the Redirection URI values for the Client pre-registered at the OpenID Provider, with the matching performed as described in Section 6.2.1 of [RFC3986] (Simple String Comparison). When using this flow, the Redirection URI SHOULD use the https scheme; however, it MAY use the http scheme, provided that the Client Type is confidential, as defined in Section 2.1 of OAuth 2.0, and provided the OP allows the use of http Redirection URIs in this case. The Redirection URI MAY use an alternate scheme, such as one that is intended to identify a callback into a native application.

state

RECOMMENDED. Opaque value used to maintain state between the request and the callback. Typically, Cross-Site Request Forgery (CSRF, XSRF) mitigation is done by cryptographically binding the value of this parameter with a browser cookie.

OpenID Connect also uses the following OAuth 2.0 request parameter, which is defined in OAuth 2.0 Multiple Response Type Encoding Practices [OAuth.Responses]:

response_mode

OPTIONAL. Informs the Authorization Server of the mechanism to be used for returning parameters from the Authorization Endpoint. This use of this parameter is NOT RECOMMENDED when the Response Mode that would be requested is the default mode specified for the Response Type.

This specification also defines the following request parameters:

nonce

OPTIONAL. String value used to associate a Client session with an ID Token, and to mitigate replay attacks. The value is passed through unmodified from the Authentication Request to the ID Token. Sufficient entropy MUST be present in the nonce values used to prevent attackers from guessing values. For implementation notes, see Section 15.5.2.

Remember also for non ‘private’ or native app types you’ll need to follow the PKCE code challenge flow.

Finally was successful in getting through the entire flow on this. Something I noted in addition to the above tutorial was needed to place the state parameter when making a request for an access token which was also returned along with the authorization code (also needed to do it pretty quickly before it timed out :slight_smile: ). really neat stuff now that i am beginning to understand how it all works.

1 Like

Hi @sjpadgett
Thanks a lot for your clarification.

Just a few additional issues I’ll happy to understand better.

  1. “Password grant” - In which step of the process I can use it? what are the parameters need to send for that? when is better to use a password instead of a token?

  2. Do you think to integrate MFA in the authentication process (for users that registered to MFA in the user table)? is it can be fit together?

1 Like

Hi @Amiel,

  1. The password grant doesn’t require an authorization server sign in, only registration. The endpoint for password is the token endpoint with a password grant type and client_id. A secret currently is not required.
curl -X POST -k -H 'Content-Type: application/x-www-form-urlencoded' 
-i 'https://localhost/oauth2/default/token' 
--data 'grant_type=password
&client_id=G2aphKFWUks8203A8JtriLnYUTZlTWHjJqIeAg5EqQs
&username=admin
&password=pass
&scope=openid api:fhir'

Response

{
  "id_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9....",
  "token_type": "Bearer",
  "expires_in": 3600,
  "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSU...",
  "refresh_token": "def50200ded9989134976302793b9cea72895d5..."
}
  1. I don’t plan to do MFA as is kind of out of scope for what we currently need for ONC. However, one could add a different third party provider to the flow and hook out of server sign in dialog. Or somewhere depending on what our current MFA requires. I’ve never looked at it and don’t know if only requires a dongle or also sends text/email etc…

edit: changed request password grant endpoint example to default/token as the most correct. Using default/password works also and is unique to openemr.

Here is a password refresh ex:

curl -X POST -k -H 'Content-Type: application/x-www-form-urlencoded'
-i 'https://localhost/oauth2/default/token' 
--data 'grant_type=refresh_token
&client_id=openemrfhir
&refresh_token=def502005fdeecf212f0158f45...'
1 Like

Just updated Authentication documentation for this feature:
openemr/API_README.md at master · openemr/openemr · GitHub

@sjpadgett , just really awesome stuff!!!

And of course the fans are happy

2DV

1 Like

Thanks for the coding/review help and especially keeping up with the darn official documenting @brady.miller.

@Amiel You folks are probably the furthest along with implementing this feature. Just wanted you to be aware that Brady and I have tightened security and updated the feature and is in master now.

I am very curious how you plan to use the feature. Are you primarily going to use with password grant or how do you plan to use the password grant?

Are you still looking into hooking in a third party provider or MFA?
Do you plan to use the public application type for your users and thus the code challenge flow?
Besides the implemented custom scopes of api:fhir, api:oemr, api:port and api:pofh are there others i’ve missed?

Feedback by anyone is appreciated before I move on to adding a SMART layer which i’ll probably start a new thread to track. SMART will not be included in our upcoming initial release of v6.0.0 but will soon follow.

Hi @sjpadgett
Thank you for being interested in us.
We are actually at the end of the development of a client-side application based on React for Emergency medical centers, the application needs to start a pilot in the next weeks therefore for now we implement Grant password authentication that matches to existing code, I hope we continue to Code grant when the project will grow up.
For MFA (we must for security requirements) we plan to expand the existing MFA found in the login of openemr to grant password in the API (for users that turn the MFA on), hopefully to create pull request soon.

Our application use a openemr API and FHIR api but most of the calls address to our zend modules that expand the api.

On this occasion I invite you to take a look on our projects, all the source is open, and from the last weekend integrated with the latest Openemr code.

backend modules -



React client -

devops tools -

Always happy about collaboration.

Best regards
Amiel

1 Like

@sjpadgett @brady.miller
Just the last issue is missing in the new authorization server - logout action. would Do you plan to write it soon?
Thanks a lot…

Thanks @Amiel for sharing what your team has been up to. Very exciting work of which exposure on this thread may help some other community members. I know I learn a couple things.

Concerning logout action, yes I hope to have a PR up tonight or tomorrow that will include logout, revoke and maybe finalize the userinfo endpoint.

I’m still a little concerned with registration and may add a few items to track.

As for MFA, I assume our current core solution meets your need although, it sure would be handy to have in authorization server for apis.

Edit: Looking at a logout, i’m somewhat undecided how best to handle in our implementation.
I don’t issue cookies to user agent/user to maintain a persistent session state. I persist in a table.
As far as server is concerned, you are signed into the server unless a refresh token has expired at which point client must sign back into server to get a new token.
So a logout for us would really just be a revoke of refresh token.

How do you see this working?

For client logout here is pattern i’m thinking. Please, anyone with further input, please chime in.

  • basic
GET https://{baseUrl}/logout?id_token_hint=${id_token}

The trusted user session associated/identified by the id token will be deleted and a confirmation displayed.

  • registered redirects request initiates a logout and redirects to the post_logout_redirect_uri. The registered post_logout_redirect_uri and the request post_logout_redirect_uri must match.
GET https://{baseUrl}/logout?
  id_token_hint=${id_token}&
  post_logout_redirect_uri=${post_logout_redirect_uri}&
  state=${state}

Both initiate a redirect to either the OP confirm dialog or the redirected endpoint depending on request.

HTTP 302 Found
Location: https://post_logout_redirect_uri/redirect&state=${state}
2 Likes

If post_logout_redirect_uris is provided during registration then after session is destroyed, server will redirect to that endpoint otherwise, a logged out message is sent to user logging out.

We maintain user sessions as trusted users where a log out is essentially removing the trusted user resulting in resource server dispatch denying any tokens, valid or not, from advancing api request until user logs back into identity server.

I don’t see any reason to revoke tokens because that would essentially just be invalidating current session the tokens were issue against. If disagreement, please comment.

@Amiel this probably is of interest you.

1 Like

Looks excellent. thank you!

Announcing the token introspection endpoint and if I may say, at one point I just wanted to have a response of yep or nope. I mean, why be so formal!:slight_smile:

  • Only access_token and refresh_tokens will be validated. I may add id_token later but, it is really not needed IMO.

  • All Http status responses will be 200 with response showing whether active and the token status. Exceptions are in the case token fails signature verification or a mangled request then you’ll see appropriate 400/401/500.

  • To fetch url from discovery json use 'introspection_endpoint' that yields something like https://localhost/oauth2/default/introspect

  • Request attributes:

Field Description Type Required
client_id Application client Id String Yes
client_secret Application Client Secret. If client has private(confidential) registration status, then a client secret is mandatory otherwise, public apps only require client_id. String Yes
token_type_hint The appropriate token hint of access_token or refresh_token String Yes
token The string value of the token returned from auth token endpoints. String Yes
curl -X POST -k -H 'Content-Type: application/x-www-form-urlencoded'
 -i  'https://localhost:port/oauth2/default/introspect' 
--data 'client_id=kbyuFDidLLm280LIwVFiazOqj...
&client_secret=khYVHgkbBBYUU...
&token_type_hint=refresh_token
&token=def50200695611d39349fad4bb913b686e1c53fc99116ebabdfcc08...
  • Responses:

Active Token

Response Body Field Value Returned
active true
status ‘active’
exp Expiry Epoch Time
sub The subject of the token. Mostly user_id UUID
scope Token scopes
client_id Application Client ID Value

Expired Token

Response Body Field Value Returned
active false
status ‘expired’
exp Expiry Epoch Time
sub The subject of the token. Mostly user_id UUID
scope Token scopes
client_id Application Client ID Value

Revoked/Logged out User Token

Response Body Field Value Returned
active false
status ‘revoked’
exp Expiry Epoch Time
sub The subject of the token. Mostly user_id UUID
scope Token scopes
client_id Application Client ID Value

Invalid Client Id or Client Secret
Also case of token client info doesn’t match trusted user. In both regards let’s not return anything useful

Response Body Field Value Returned
active false
status ‘invalid’
  • A refresh_token example response
{
    "active": true,
    "status": "active",
    "scope": "openid email phone api:fhir api:pofh site:default",
    "client_id": "kbyuFDidLLm280LIwVFiazOqjO3ty8KH",
    "exp": 1614792378,
    "sub": "91e65743-aa8c-4a7e-a183-706912c92436"
}
  • A token invalid client example response
{"active":false}

So I think this gives a good overview of this endpoint. Comments welcomed.

Temporary note: I got a jump start on documenting this for comments with the PR going up within a day and in master shortly thereafter. I’ll post back once in master.

1 Like