Netflix / lemur

Repository for the Lemur Certificate Manager
Apache License 2.0
1.72k stars 324 forks source link

OAUTH2 with Azure issues #3656

Open rvaneerdewijk opened 3 years ago

rvaneerdewijk commented 3 years ago

Hi, I'm struggling to get OAUTH2 authentication working with Azure. It's possible I've left out important fields or are using the wrong values.

Config file options (with values changed for security reasons):

ACTIVE_PROVIDERS = ["oauth2"] OAUTH2_SECRET = 'e0~r14.IIpG~-daPyAR2.QjsDgxaWjfo91' OAUTH2_ACCESS_TOKEN_URL = "https://login.microsoftonline.com/1ede324e-872d-4f4b-8e56-ce3ed381b23c/oauth2/token" OAUTH2_NAME = "Azure" OAUTH2_CLIENT_ID = "12354aa4-116b-4d79-9b4a-0f4d3941f87b" OAUTH2_REDIRECT_URI = "https://lemur.ourdomain.com/api/1/auth/oauth2" OAUTH2_AUTH_ENDPOINT = "https://login.microsoftonline.com/1ede324e-872d-4f4b-8e56-ce3ed381b23c/oauth2/authorize" OAUTH2_VERIFY_CERT = True

This adds an option to the login page. When I click the new button for logging into Azure, it opens up a pop-up verifying which Azure account I want to use. I select the account, and I immediately get the warning "Whoa there ... The supplied credentials are invalid". The log file has the following entries:

2021-06-30 14:43:15,772 DEBG 'lemur-web' stdout output: Exception on /api/1/auth/oauth2 [POST] Traceback (most recent call last): File "/www/lemur/lib/python3.8/site-packages/flask/app.py", line 1950, in full_dispatch_request rv = self.dispatch_request() File "/www/lemur/lib/python3.8/site-packages/flask/app.py", line 1936, in dispatch_request return self.view_functionsrule.endpoint File "/www/lemur/lib/python3.8/site-packages/flask_restful/init.py", line 467, in wrapper resp = resource(*args, kwargs) File "/www/lemur/lib/python3.8/site-packages/flask/views.py", line 89, in view return self.dispatch_request(*args, *kwargs) File "/www/lemur/lib/python3.8/site-packages/flask_restful/init.py", line 582, in dispatch_request resp = meth(args, kwargs) File "/www/lemur/lemur/auth/views.py", line 548, in post id_token, access_token = exchange_for_access_token( File "/www/lemur/lemur/auth/views.py", line 80, in exchange_for_access_token id_token = r.json()["id_token"] KeyError: 'id_token'

Is there a best practices document for setting up Lemur with Azure auth?

Thanks

hosseinsh commented 3 years ago

Hi @rvaneerdewijk,

The Azure Plugin is a community contribution. Marking this as question, in case someone has familiarity with OAUTH2 in Azure

rvaneerdewijk commented 3 years ago

Thanks, @hosseinsh . I had no idea there was a community Azure plugin. I was just attempting to use the built-in OAUTH2 functionality. Do you know where I might find this?

hosseinsh commented 3 years ago

This is the Azure destination plugin in Lemur https://github.com/Netflix/lemur/blob/192112f4ce735bfba0c443f7a7ee8108777f4faf/lemur/plugins/lemur_azure_dest/plugin.py#L7-L8

rvaneerdewijk commented 3 years ago

@hosseinsh from what I'm reading, "destination" is not what I'm looking for. I'm looking to have it so Azure AD users can log into Lemur. So far OAuth2 sounds like the closest fit but I'm not sure of the specific parameters.

hosseinsh commented 3 years ago

Oh, sorry I got it the wrong way that you are integrating with azure to upload certificates.

Correct, Lemur does provide Oauth2 support for the Lemur login.

would you make sure to not log any secrets and redact the value from OAUTH2_SECRET.

rvaneerdewijk commented 3 years ago

@hosseinsh those are fake values up there ^^ so nothing to worry about. I made sure to replace them when posting.

rvaneerdewijk commented 3 years ago

Bumping this... anyone? Is anyone using OAUTH2 at all? And how does Lemur decide who can/can't log into Lemur and what role they belong to?

rvaneerdewijk commented 3 years ago

Still stuck here.

I've managed to find some more URLs related to OAUTH2 for Azure:

OAUTH2_JWKS_URL = "https://login.microsoftonline.com/common/discovery/keys" OAUTH2_USER_API_URL = "https://login.microsoftonline.com/common/openid/userinfo"

... but these change nothing. When I point to the OAUTH2 v2 URLs instead of v1, it doesn't throw an error, but it doesn't log me in either. It just sits there after authentication and there are no helpful messages in the log.

Surely somebody knows something.

These URLs show the configuration options for OAUTH2:

v1: https://login.microsoftonline.com/common/.well-known/openid-configuration v2: https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration

When happens in Lemur when somebody authenticates via OAUTH2? Is a user account automatically created? Does the user need to be pre-populated? I'm not sure what Lemur expects.

Thanks

havron commented 3 years ago

Hi @rvaneerdewijk can you try this?

https://github.com/Netflix/lemur/issues/3705#issuecomment-892675964

It's possible that Azure implemented the OAuth2 RFC in a case-sensitive way.

rvaneerdewijk commented 3 years ago

Hi @rvaneerdewijk can you try this?

#3705 (comment)

It's possible that Azure implemented the OAuth2 RFC in a case-sensitive way.

Changing the headers line in views.py to:

"Authorization": "Basic {0}".format(basic.decode("utf-8")),

... didn't seem to solve it. But I may be missing something elsewhere.

The error seems to have changed:

2021-08-04 15:41:35,642 DEBG 'lemur-web' stdout output: [2021-08-04 15:41:35,629] ERROR in app: Exception on /api/1/auth/oauth2 [POST] Traceback (most recent call last): File "/www/lemur/lib/python3.8/site-packages/flask/app.py", line 1950, in full_dispatch_request rv = self.dispatch_request() File "/www/lemur/lib/python3.8/site-packages/flask/app.py", line 1936, in dispatch_request return self.view_functionsrule.endpoint File "/www/lemur/lib/python3.8/site-packages/flask_restful/init.py", line 467, in wrapper resp = resource(*args, kwargs) File "/www/lemur/lib/python3.8/site-packages/flask/views.py", line 89, in view return self.dispatch_request(*args, *kwargs) File "/www/lemur/lib/python3.8/site-packages/flask_restful/init.py", line 582, in dispatch_request resp = meth(args, kwargs) File "/www/lemur/lemur/auth/views.py", line 562, in post user, profile = retrieve_user(user_api_url, access_token) File "/www/lemur/lemur/auth/views.py", line 142, in retrieve_user profile = r.json() File "/www/lemur/lib/python3.8/site-packages/requests/models.py", line 900, in json return complexjson.loads(self.text, **kwargs) File "/usr/lib/python3.8/json/init.py", line 357, in loads return _default_decoder.decode(s) File "/usr/lib/python3.8/json/decoder.py", line 337, in decode obj, end = self.raw_decode(s, idx=_w(s, 0).end()) File "/usr/lib/python3.8/json/decoder.py", line 355, in raw_decode raise JSONDecodeError("Expecting value", s, err.value) from None json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)

2021-08-04 15:41:35,645 DEBG 'lemur-web' stdout output: Exception on /api/1/auth/oauth2 [POST] Traceback (most recent call last): File "/www/lemur/lib/python3.8/site-packages/flask/app.py", line 1950, in full_dispatch_request rv = self.dispatch_request() File "/www/lemur/lib/python3.8/site-packages/flask/app.py", line 1936, in dispatch_request return self.view_functionsrule.endpoint File "/www/lemur/lib/python3.8/site-packages/flask_restful/init.py", line 467, in wrapper resp = resource(*args, kwargs) File "/www/lemur/lib/python3.8/site-packages/flask/views.py", line 89, in view return self.dispatch_request(*args, *kwargs) File "/www/lemur/lib/python3.8/site-packages/flask_restful/init.py", line 582, in dispatch_request resp = meth(args, kwargs) File "/www/lemur/lemur/auth/views.py", line 562, in post user, profile = retrieve_user(user_api_url, access_token) File "/www/lemur/lemur/auth/views.py", line 142, in retrieve_user profile = r.json() File "/www/lemur/lib/python3.8/site-packages/requests/models.py", line 900, in json return complexjson.loads(self.text, kwargs) File "/usr/lib/python3.8/json/init.py", line 357, in loads return _default_decoder.decode(s) File "/usr/lib/python3.8/json/decoder.py", line 337, in decode obj, end = self.raw_decode(s, idx=_w(s, 0).end()) File "/usr/lib/python3.8/json/decoder.py", line 355, in raw_decode raise JSONDecodeError("Expecting value", s, err.value) from None json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0) Exception on /api/1/auth/oauth2 [POST] Traceback (most recent call last): File "/www/lemur/lib/python3.8/site-packages/flask/app.py", line 1950, in full_dispatch_request rv = self.dispatch_request() File "/www/lemur/lib/python3.8/site-packages/flask/app.py", line 1936, in dispatch_request return self.view_functions[rule.endpoint](req.view_args) File "/www/lemur/lib/python3.8/site-packages/flask_restful/init.py", line 467, in wrapper resp = resource(*args, kwargs) File "/www/lemur/lib/python3.8/site-packages/flask/views.py", line 89, in view return self.dispatch_request(*args, *kwargs) File "/www/lemur/lib/python3.8/site-packages/flask_restful/init.py", line 582, in dispatch_request resp = meth(args, kwargs) File "/www/lemur/lemur/auth/views.py", line 562, in post user, profile = retrieve_user(user_api_url, access_token) File "/www/lemur/lemur/auth/views.py", line 142, in retrieve_user profile = r.json() File "/www/lemur/lib/python3.8/site-packages/requests/models.py", line 900, in json return complexjson.loads(self.text, **kwargs) File "/usr/lib/python3.8/json/init.py", line 357, in loads return _default_decoder.decode(s) File "/usr/lib/python3.8/json/decoder.py", line 337, in decode obj, end = self.raw_decode(s, idx=_w(s, 0).end()) File "/usr/lib/python3.8/json/decoder.py", line 355, in raw_decode raise JSONDecodeError("Expecting value", s, err.value) from None json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)

havron commented 3 years ago

Hi @rvaneerdewijk - nice! That adjustment seems to have gotten you closer to the end of the OAuth2 flow. This new error is during retrieval of the user profile after the token exchange, which seems to have succeeded now. I haven't used Azure before, but maybe you want to add that authorization bearer when retrieving the user profile by ensuring that this line runs:

https://github.com/Netflix/lemur/blob/26601920820e6138fe78b7d5fc9af18019af5e2b/lemur/auth/views.py#L134

Not sure if that works but worth a try.

Syesad commented 3 years ago

Hi @rvaneerdewijk - Did you manage to fix this problem? I am encountering similar issue when integrating with OKTA. I don't think, the request is being successfully sent to OKTA. If you have managed to fix this problem, please share the resolution.

As Sam suggested, I have added header, but it doesn' t seem to fix the problem. I tried to print the response, and looks like it is throwing 400 error. So, I am suspecting that the request is not being prepared correctly at the lemur level.

Also, I don't see any logs on the OKTA side

rvaneerdewijk commented 3 years ago

Hi @rvaneerdewijk - nice! That adjustment seems to have gotten you closer to the end of the OAuth2 flow. This new error is during retrieval of the user profile after the token exchange, which seems to have succeeded now. I haven't used Azure before, but maybe you want to add that authorization bearer when retrieving the user profile by ensuring that this line runs:

https://github.com/Netflix/lemur/blob/26601920820e6138fe78b7d5fc9af18019af5e2b/lemur/auth/views.py#L134

Not sure if that works but worth a try.

I wouldn't know how to check that. I've added "access tokens" along with "ID tokens" on the Azure side, but so far it seems to be throwing the same errors. Pretend I don't know anything about OAUTH2 or Flask development (because I don't... I'm bumbling my way through this. I struggle to read code written by "real" programmers that doesn't look like QBasic).

rvaneerdewijk commented 3 years ago

Hi @rvaneerdewijk - Did you manage to fix this problem? I am encountering similar issue when integrating with OKTA. I don't think, the request is being successfully sent to OKTA. If you have managed to fix this problem, please share the resolution.

As Sam suggested, I have added header, but it doesn' t seem to fix the problem. I tried to print the response, and looks like it is throwing 400 error. So, I am suspecting that the request is not being prepared correctly at the lemur level.

Also, I don't see any logs on the OKTA side

Nope, not yet. I 100% need my hand held here.

rvaneerdewijk commented 3 years ago

Hi @rvaneerdewijk - nice! That adjustment seems to have gotten you closer to the end of the OAuth2 flow. This new error is during retrieval of the user profile after the token exchange, which seems to have succeeded now. I haven't used Azure before, but maybe you want to add that authorization bearer when retrieving the user profile by ensuring that this line runs: https://github.com/Netflix/lemur/blob/26601920820e6138fe78b7d5fc9af18019af5e2b/lemur/auth/views.py#L134

Not sure if that works but worth a try.

I wouldn't know how to check that. I've added "access tokens" along with "ID tokens" on the Azure side, but so far it seems to be throwing the same errors. Pretend I don't know anything about OAUTH2 or Flask development (because I don't... I'm bumbling my way through this. I struggle to read code written by "real" programmers that doesn't look like QBasic).

Just having a look at this line... it looks like it is preceded by:

if current_app.config.get("PING_INCLUDE_BEARER_TOKEN"):

I don't have any PING-related configuration options set because I didn't think it would apply here. So I would think the line quoted above would not run.

rvaneerdewijk commented 3 years ago

I may be close to getting this. I commented out the line:

if current_app.config.get("PING_INCLUDE_BEARER_TOKEN"):

and collapsed the indent for the line immediately after it:

headers = {"Authorization": f"Bearer {access_token}"}

This changed my error to:

2021-08-10 17:05:31,237 DEBG 'lemur-web' stdout output:
Exception on /api/1/auth/oauth2 [POST]
Traceback (most recent call last):
  File "/www/lemur/lib/python3.8/site-packages/flask/app.py", line 1950, in full_dispatch_request
    rv = self.dispatch_request()
  File "/www/lemur/lib/python3.8/site-packages/flask/app.py", line 1936, in dispatch_request
    return self.view_functions[rule.endpoint](**req.view_args)
  File "/www/lemur/lib/python3.8/site-packages/flask_restful/__init__.py", line 467, in wrapper
    resp = resource(*args, **kwargs)
  File "/www/lemur/lib/python3.8/site-packages/flask/views.py", line 89, in view
    return self.dispatch_request(*args, **kwargs)
  File "/www/lemur/lib/python3.8/site-packages/flask_restful/__init__.py", line 582, in dispatch_request
    resp = meth(*args, **kwargs)
  File "/www/lemur/lemur/auth/views.py", line 562, in post
    user, profile = retrieve_user(user_api_url, access_token)
  File "/www/lemur/lemur/auth/views.py", line 144, in retrieve_user
    user = user_service.get_by_email(profile["email"])
KeyError: 'email'

I added 'email' to the optional claims in Azure. Now my error is:

2021-08-10 17:08:55,746 DEBG 'lemur-web' stdout output:
[2021-08-10 17:08:55,744] WARNING in views: OAuth State token is too stale.
OAuth State token is too stale.
OAuth State token is too stale.

This looks a LOT better.

If I restart the Lemur service, I get the complaint about 'email' again on first login attempt, and then the state token error on subsequent attempts.

Any ideas? Am I getting closer or making it worse?

havron commented 3 years ago

@rvaneerdewijk nice!! It looks like you're getting to the very end of the flow. For the stale OAuth token issue, that warning is intended to be thrown to prevent users from returning state tokens that are too old. Is the login with Azure taking longer than 15 seconds (the default tolerance)? Can you try setting OAUTH_STATE_TOKEN_STALE_TOLERANCE_SECONDS in your config to something higher like 60 seconds?

Also, I would note that the state token staleness check occurs before the errors you'd seen previously. Which would indicate that you've been able to get past this error in the past.

rvaneerdewijk commented 3 years ago

I've tried setting the OAUTH_STATE_TOKEN_SECRET and OAUTH_STATE_TOKEN_STALE_TOLERANCE_SECONDS parameters.

I'm still ending up with the 'email' error.

The "OAuth State token is too stale" error seems to be delayed by however many seconds I put in the OAUTH_STATE_TOKEN_STALE_TOLERANCE_SECONDS parameter. Right now I'm experimenting with 60.

havron commented 3 years ago

For the email error -- can you see what's in the full profile dict by printing this line? The KeyError indicates that Azure is not including any data under "email" in its response to your /userinfo request, but it's possible that user emails are being returned under a different key name like "mail" or "userPrincipalName"

rvaneerdewijk commented 3 years ago

I've exported the contents of 'profile' by adding the following lines afterward:

    with open('/tmp/profile_json.txt', 'w') as jsonfile:
        jsonfile.write(json.dumps(profile))

Sure enough, "email" is not there. What is there:

aio amr family_name given_name ipaddr name oid onprem_sid rh sub tid unique_name upn uti ver

If modifying the code to use upn is easy, that's preferable and should be more consistent.

rvaneerdewijk commented 3 years ago

I guess that would mean modifying the OAuth scope to change "email" to "upn" and any reference to profile["email"] to profile["upn"]?

rvaneerdewijk commented 3 years ago

Actually, figured out a much simpler solution... followed up the line:

profile = r.json()

with:

profile["email"] = profile["upn"]

I am able to log in now!

havron commented 3 years ago

Yay! @hosseinsh may have additional thoughts, but it could make sense to raise a PR for this to support future Azure users who want to use OAuth2. It sounds like the Azure-specific authentication changes that were needed to get Lemur working were these:

  1. https://github.com/Netflix/lemur/blob/26601920820e6138fe78b7d5fc9af18019af5e2b/lemur/auth/views.py#L69 changing "authorization": "basic {0}" to "Authorization": "Basic {0}" because Azure's oauth2 implementation is case-sensitive on auth headers
  2. Perhaps making a new config parameter, USER_PROFILE that is by default set to "email" (this way, in lemur/auth/views.py, profile email access for user creation would happen as profile[current_app.config.get("USER_PROFILE")]). The config would include a comment above it about Azure needing to change it to "upn".
  3. Then finally, Azure, like Ping, expects an Authorization Bearer token when retrieving user profile details. So setting a new config variable called AZURE_INCLUDE_BEARER_TOKEN could make sense.

Does that sound right?

rvaneerdewijk commented 3 years ago

Correct that all sounds right.

On the Azure side, the app registration is configured as follows...

Redirect URI = same as OAUTH2_REDIRECT_URI below

Checkboxes checked:

API Permissions:

User.Read User.ReadBasic.All

The file lemur.conf.py needs lines as follows:

OAUTH2_SECRET = '[client secrets generated in Azure' OAUTH2_ACCESS_TOKEN_URL = "https://login.microsoftonline.com/[Directory (tenant) ID]/oauth2/token" OAUTH2_NAME = "Azure" <--- name this whatever you want OAUTH2_CLIENT_ID = "[Application (client) ID]" OAUTH2_REDIRECT_URI = "https://[your Lemur hostname]/api/1/auth/oauth2" OAUTH2_AUTH_ENDPOINT = "https://login.microsoftonline.com/[Directory (tenant) ID]/oauth2/authorize" OAUTH2_JWKS_URL = "https://login.microsoftonline.com/common/discovery/keys" OAUTH2_USER_API_URL = "https://login.microsoftonline.com/[Directory (tenant) ID]/openid/userinfo" OAUTH2_VERIFY_CERT = True OAUTH_STATE_TOKEN_SECRET = b'[generated as per documentation]' OAUTH_STATE_TOKEN_STALE_TOLERANCE_SECONDS = 60 <--- this could probably be less

... replacing the parameters in square brackets.

joeinfor7685 commented 3 years ago

We are currently deploying Lemur to handle our certs and ran into the same issue with OAUTH2 authentication using Azure. We deployed the Flask API and the UI separately under different domain names, so we had to point the PING_REDIRECT_URI to the UI and the PING_URL to the API. We were able to get everything to work using the following config:

PING_SECRET = [client secret generated in azure] PING_ACCESS_TOKEN_URL = https://login.microsoftonline.com/[tenant_id]/oauth2/v2.0/token PING_USER_API_URL = https://graph.microsoft.com/oidc/userinfo PING_JWKS_URL = https://login.microsoftonline.com/[tenant_id]/discovery/v2.0/keys PING_NAME = [lemur display name] PING_CLIENT_ID = [azure application client id] PING_URL = https://[api domain name]/api/1/auth/ping PING_REDIRECT_URI = https://[ui domain name] PING_AUTH_ENDPOINT = https://login.microsoftonline.com/[tenant_id]/oauth2/v2.0/authorize PING_INCLUDE_BEARER_TOKEN = True OAUTH_STATE_TOKEN_SECRET = b'[generated as per documentation]'

Azure Settings:

Authentication:

API Permissions:

NOTE: Using https://graph.microsoft.com/oidc/userinfo for the User API URL will return the email without having to change to upn

joeinfor7685 commented 3 years ago

@havron - Thanks for posting this! We had to change the "authorization": "basic {0}" to "Authorization": "Basic {0}" as well.

https://github.com/Netflix/lemur/blob/26601920820e6138fe78b7d5fc9af18019af5e2b/lemur/auth/views.py#L69

@hosseinsh - Would like to suggest another potential PR to address the separate front-end and back-end OAUTH2 issue. We could add the following config params PING_URL and OAUTH2_URL. Both params could be defaulted to PING_REDIRECT_URI and OAUTH2_REDIRECT_URI to ensure backwards compatibility.

https://github.com/Netflix/lemur/blob/c6a13bf25f4edd11f67f8beb54313b75287c8bd4/lemur/auth/views.py#L635

Changed to: "url": current_app.config.get("PING_URL", current_app.config.get("PING_REDIRECT_URI")),

https://github.com/Netflix/lemur/blob/c6a13bf25f4edd11f67f8beb54313b75287c8bd4/lemur/auth/views.py#L653

Changed to: "url": current_app.config.get("OAUTH2_URL", current_app.config.get("OAUTH2_REDIRECT_URI")),

hosseinsh commented 3 years ago

@JoeMcRobot that sounds good to me, happy to review the respective PR.