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

How to configure OpenEMR 6.x with Keycloak (or any OAuth2 server)

Hello community,

I want to use OpenEMR for its ability to provide FHIR resources. For this, I need to configure an authentication server (OAuth2).

I installed OpenEMR 6.0 with docker
I installed Keycloak (version 12.0.1) as an authentication server (docker too).

A priori the installation is operational.

The problem is, this is my first experience with OAuth2 and the Keycloak server offers a lot of options (too many for me).

So if there are people in the community who can guide me in configuring an OAuth2 server, I would be grateful to you and I think that through this support many other people will be helped.

Thanks!

Hi @david.ouagne,
OpenEMR has implemented a OAuth2/OIDC server as part of it’s offering in our v6.0.0 release. Also released in this version is the SMART on FHIR EHR launch with Standalone launch coming in first or second patch.

See openemr/API_README.md at master · openemr/openemr · GitHub
openemr/FHIR_README.md at master · openemr/openemr · GitHub

Thanks @sjpadgett, I had seen this information but without understanding it.

I changed the configuration of my instance and in particular the “Site Address” property with this URL: https://localhost:443.

I can’t get an access token: for testing i am using postman and i dont know how to configure my query. Can you tell me how to do it?

Hello There,

Hope you are doing well!

I’d Glad to assist you, Please reach to me at seth@cisinlabs.com

Very Best Regards
Seth R

Hi @seth_cis, thank you for your proposal, but in my conception of open source I think that sharing user feedback is very important.

Hi @sjpadgett, I advanced on my problem:
Based on V6 Authorization and API changes afoot, I understood the necessary sequence to authenticate with OAuth2.

  1. Registration
  2. Authorize
  3. Get token (grant_type=authorization_code or grant_type=password)
  4. API request + token

Despite this, I cannot request my patient through the FHIR API.

I use this

curl -X GET -k -i 'https://localhost/oauth2/default/authorize?
response_type=code
&client_id=P5s9xNlJSSizU0ArnTuxHHkBp5Hhy3bBcvr-7ABiczw
&scope=openid email phone address api:pofh api:fhir

to obtain a code: https://client.example.org/callback?code=18d9924ef7bdb9a005d3cca5bbc9045812851816da139da3cba29ebf7e44f776a8e43809a7733eea2b6a9dc68aa9d80e837edf850aeb1e8b6f9bf34b0ca2671684d41b26d1c97dd75224f1f318d5771beebbf8e29d99807a2ddc1f4215c7abab4f663136e514337fef9b4fb33594a6ce154220eb272bb4cb99a9f2f00bbf52fd403faa75bf636e8b73fc95d756bfd244103ce34678ad628235c9b296a02b8f44dd29628ab66e46b7fa4bd66d92d6e9f630b5ce174dbf1f36fd303737e120bf2f0737740783da2a2665a82477e43699837a05c04105888f05326e74f25f5612ebf7259b8d5430d08

Next with this code:

curl -X POST -k -H ‘Content-Type: application/x-www-form-urlencoded’
-i https://localhost/oauth2/default/token
–data 'grant_type=authorization_code
&client_id=P5s9xNlJSSizU0ArnTuxHHkBp5Hhy3bBcvr-7ABiczw
&client_secret=7a2f67a2-20fe-4cf4-922c-9f5af52fb039
&code=18d9924ef7bdb9a005d3cca5bbc9045812851816da139da3cba29ebf7e44f776a8e43809a7733eea2b6a9dc68aa9d80e837edf850aeb1e8b6f9bf34b0ca2671684d41b26d1c97dd75224f1f318d5771beebbf8e29d99807a2ddc1f4215c7abab4f663136e514337fef9b4fb33594a6ce154220eb272bb4cb99a9f2f00bbf52fd403faa75bf636e8b73fc95d756bfd244103ce34678ad628235c9b296a02b8f44dd29628ab66e46b7fa4bd66d92d6e9f630b5ce174dbf1f36fd303737e120bf2f0737740783da2a2665a82477e43699837a05c04105888f05326e74f25f5612ebf7259b8d5430d08

I receve bad request (400).

if I test with password, I receve a token but the scope field of the response does not correspond to the scope of the request.

in.scope: openid email phone address api:fhir
out.scope: openid email phone address

using this token, I cannot make a request on the FHIR API.

One more thing, when I use the authorization request and there is a drug in the inventory then I get this result:

Query Error

ERROR: query failed: UPDATE drugs SET uuid = ? WHERE id = ?

Error: Unknown column ‘id’ in ‘where clause’

/var/www/localhost/htdocs/openemr/src/Common/Uuid/UuidRegistry.php at 155:sqlQueryNoLog
/var/www/localhost/htdocs/openemr/src/Services/PrescriptionService.php at 40:createMissingUuids()
/var/www/localhost/htdocs/openemr/src/Services/FHIR/FhirMedicationRequestService.php at 29:__construct()
/var/www/localhost/htdocs/openemr/src/RestControllers/RestControllerHelper.php at 133:__construct()
/var/www/localhost/htdocs/openemr/src/RestControllers/RestControllerHelper.php at 216:setSearchParams(MedicationRequest,Array,OpenEMR\Services\FHIR\Fhir)
/var/www/localhost/htdocs/openemr/src/Common/Auth/OpenIDConnect/Repositories/ScopeRepository.php at 508:getCapabilityRESTJSON(Array)
/var/www/localhost/htdocs/openemr/src/Common/Auth/OpenIDConnect/Repositories/ScopeRepository.php at 682:getCurrentSmartScopes()
/var/www/localhost/htdocs/openemr/src/Common/Auth/OpenIDConnect/Repositories/ScopeRepository.php at 53:buildScopeValidatorArray()
/var/www/localhost/htdocs/openemr/vendor/league/oauth2-server/src/Grant/AbstractGrant.php at 297:getScopeEntityByIdentifier(openid)
/var/www/localhost/htdocs/openemr/vendor/league/oauth2-server/src/Grant/AuthCodeGrant.php at 277:validateScopes(Array,https://client.example.org/callback)
/var/www/localhost/htdocs/openemr/vendor/league/oauth2-server/src/AuthorizationServer.php at 157:validateAuthorizationRequest

Regards,

1 Like

David if you registered your client as a confidential app then to get your Access Token (in the authorization_grant flow) you have to pass in the client_id & client_secret using the HTTP Authorization header. This is done by base64 encoding those values in the following format client_id:client_secret and passing the header like so:

-H "Authorization: Basic <base64encoding of client_id:client_secret>"

I believe you can also attempt to use curl with the -u flag which defaults to basic authentication with a username:password combo

curl -u client_id:client_secret -X POST -k -H ....

I don’t know why 6.0 FHIR is not giving you back the api:fhir but in the next patch/release the default api will be api:fhir so that will be less of an issue. By the way you should be aware that the server will not always return to you the same scopes you request as part of your authorization. OpenEMR may deny certain scope requests based upon the permissions of the user that logs in.

I don’t know what is going on with the MedicationRequest resource, that’s a question for @sjpadgett and whoever else implemented that particular resource.

BTW I read you are using Postman, Postman very easily lets you configure the Basic Authentication.

Hope that helps a bit.

Hi @adunsulag,

Thank you for your answer.

Seeing your answer, I thought it was a great suggestion! But the result is exactly the same.

The OAuth2 / OIDC server implements the client_secret_post for token_endpoint_auth_method, maybe my problem is not there?

Indeed, I use Postman and not curl but to illustrate the requests curl is practical but it is possible that my CURL requests are not exact.

Regards,

@david.ouagne I would turn on the system debug logging and see what you are getting back on your apache server error logs. In Globals Settings -> Logging -> System Error Logging Options change it from Standard Error Logging to Debug Error Logging. You’ll get a lot more information from your apache error logs that way.

I would guess that perhaps your redirect_uri doesn’t match with what you registered with, but the enhanced debug logs might help track that down.

ah I have new information!


Notice: Key file "file:///var/www/localhost/htdocs/openemr/sites/default/documents/certificates/oaprivate.key" permissions are not correct, recommend changing to 600 or 660 instead of 700 in /var/www/localhost/htdocs/openemr/vendor/league/oauth2-server/src/CryptKey.php on line 63

but that does not solve the problem …

in apache log, I have : [Tue Jan 19 15:02:55.885453 2021] [php7:notice] [pid 79] [client 172.20.0.1:52780] [2021-01-19T15:02:55.885407+00:00] OpenEMR.DEBUG: AuthorizationController->oauthAuthorizeToken() OAuthServerExceptio
n occurred {“message”:“Bad request”,“stack”:"#0 /var/www/localhost/htdocs/openemr/oauth2/authorize.php(73): OpenEMR\\RestControllers\\AuthorizationController->oauthAuthorizeToken()\n#1 {main}
"} []

Looking at the source code, I think this block is problematic in my test:

if (($this->grantType === ‘authorization_code’) && empty($_SESSION[‘csrf’])) {
// the saved session was not populated as expected
throw new OAuthServerException(‘Bad request’, 0, ‘invalid_request’, 400);
}

I wonder how to manage the notion of session in my example …

The state parameter is important, it must be at the time of authorization and then in the token request!

Here I am, thank you for the tip to activate the logs, it’s a good way to understand where the problem comes from.

Regards,

Yes the state param is used to mitigate Man In The Middle Attacks to the client. Glad that you are making headway. The next patch will include even more detailed logs for the API requests as some of the OAUTH2 Exception logs are not as detailed as we wanted them to be.

Yes that would be a good thing. Do you know if in the roadmap it is expected to be fully compliant with the HL7 smart app launch specifications and also the backend service part?

Regards,

I’m on the same boat as you trying to wrap my head around how this new API flow works and getting nowhere fast.

What method are you trying to use to authenticate? I’m trying to use the password grant unsafe method but have had no luck with that either :-/

How would one go about allowing an existing user access to the API similar to how it worked in 5.0.2?

yep,

There is definitely a learning curve. Recommend checking out the following simple oauth client to get things working:
GitHub - jumbojett/OpenID-Connect-PHP: Minimalist OpenID Connect client

Then steps are for the code request (rec using the online demo first to see how things work, since that should work on first try):

  1. Install the above client locally
  2. Use this client.php script: https://gist.github.com/bradymiller/1d3f06b3c8a2c420204f5ed28d2bd3d6
  3. Create a client via postman/curl (again, do the online demo first just to see it working https://eight.openemr.io/openemr/oauth2/default/registration):
    openemr/API_README.md at master · openemr/openemr · GitHub (ensure use the correct redirect_uris; if have client.php running locally, then would be something like https://localhost/oauth/client.php)
  4. Then place the client_id and the client_secret in the client.php script (and run it from browser). also ensure set $test to ‘non-local’ to use the online demo (note there is a local setting there that can use when then wish to try to get working on your instance).

If using the password grant method (this should only be for development purposes and not production), then do as above (step 3) in getting the client_id from online demo and then should work on first try if you hit the online demo (https://eight.openemr.io/openemr/oauth2/default/token) with (populate below request with the client_id):
openemr/API_README.md at master · openemr/openemr · GitHub

Thanks for the info @brady.miller so this is where it gets confusing I have to create a client to get a client_id then how would go about logging in again with username/password without the client_id?

Does one have to do this whole registration process all over again?

I know there’s a refresh token grant but that also requires you to know the client_id so this is where my confusion/disconnect comes from.

Also, what are these 2 screens meant to be used for?

https://localhost//interface/smart/register-app.php - I know this is meant to register a client

https://localhost/oauth2/default/provider/login - not sure what this one does tbh

Thanks for any info/clarification :pray:

hi @benmarte ,
Your app/client would store the client id (and secret when applicable) and use that when making requests.

https://localhost//interface/smart/register-app.php is used in following workflow:
openemr/API_README.md at master · openemr/openemr · GitHub

https://localhost/oauth2/default/provider/login is used in the workflow discussed above (code request with those 4 steps)