Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Azure AD Auth Provider #1990

Closed

Conversation

theplatformer
Copy link
Contributor

Hey, I just made a Pull Request!

I've managed to get a semi-working implementation going for Azure AD as an OIDC auth provider but have hit some blockers. Not so much with my understanding of how Backstage auth works, though a few things go a bit beyond me, more with how passport-azure-ad behaves.

Opening this PR as draft to share what I've done so far and hopefully get some help on resolving what is left and finishing things off. I have added a bunch of comments to the changes to add some more info and get some feedback on how I've gone about things.

Progress So Far

Azure option added to Sign In Page...

azure-signin-page

Azure AD login prompt...

azure-ad-login-window

Azure AD consent prompt...

azure-ad-consent-window

Header message with Backstage ID, using local part of email...

header-message

User settings showing Backstage profile display name and picture, plus entry for Azure provider...

user-settings

Outstanding Issues

Refresh Token Strategy

Hitting refresh attempts to call the /refresh endpoint which triggers executeRefreshTokenStrategy, which currently doesn't do anything so you are returned to the Sign In Page and need to auth again.

Looking at the Google implementation it looks like passport-google-oauth20 uses passport-oauth2 under the hood which provides the getAccessToken function that handles exchanging the refresh token, but that is as far as I got.

There is an open issue for passport-azure-ad to support refreshing tokens (AzureAD/passport-azure-ad/issues/297), though it was opened over 3 years ago with no progress as yet. There are a number of threads around the place talking to using MSAL.js on top of Passport.js to handle refreshing tokens but I'm not sure how that would work with Backstage.

Hijacked State Parameter

Backstage uses the state parameter to encode a nonce value and the environment and then verifies the return of that when handling the auth response. passport-azure-ad hijacks the state parameter and builds its own value which is what is then returned.

"Custom" state can be provided through a customState parameter. Doing this...

const providerOptions = {
  ...options,
  prompt: 'consent',
  customState: options.state,
};
return await executeRedirectStrategy(req, this._strategy, providerOptions);

passport-azure-ad then sets the state parameter to this...

let state = params.state = oauthConfig.customState ? ('CUSTOM' + aadutils.uid(32) + oauthConfig.customState) : aadutils.uid(32);

Which gives us a return value of...

CUSTOM9K1R2fWvHxxD2KB4RmDKsmhuBNGmw3QHnonce%3Da0Jzscgg7yAx%252FnncHa3CTg%253D%253D%26env%3Ddevelopment

Which then causes readState in OAuthProvider to throw an invalid state error. We effectively need to .substring(38) this returned state value to get the original value that Backstage is expecting.

This is obviously unique to the Azure provider and the others need the readState behaviour to remain unchanged. So the question is how best to handle this?

Core-API Implementation defaultScopes?

What are the defaultScopes defined as part of sessionManager within AzureAuth.ts used for?

I ask as they don't seem to have an affect when working with passport-azure-ad, as it needs scopes declared in the createAzureProvider function and passed to new AzureAuthProvider as part of the options. This seems to be different from the other provider implementations which don't have scopes defined within provider.ts.

Wondering whether that is down to my implementation, and/or what fallout there might be that I haven't realised.

Thanks!

✔️ Checklist

  • All tests are passing yarn test
  • Screenshots attached (for UI changes)
  • Relevant documentation updated
  • Prettier run on changed files
  • Tests added for new functionality
  • Regression tests added for bug fixes

@@ -41,8 +41,12 @@ export type Options = {
};

const readState = (stateString: string): OAuthState => {

// This is hardcoded for Azure (breaks other providers) until workaround is found
const fixedState = stateString.substring(38)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the hardcoded workaround I've put in to get the Azure provider functional, needs a proper solution.


const sessionManager = new RefreshingAuthSessionManager({
connector,
defaultScopes: new Set(['openid', 'offline_access', 'profile', 'email']),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have set these to match what is defined in the backend provider.ts file, but unsure of their use or impact on things as they don't seem to have an affect on passport-azure-ad behaviour.

import { Config } from '@backstage/config';
import passport from 'passport';

import got from 'got';
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Used to fetch the user's photo, which is not returned in the token and must be retrieved from the Microsoft Graph API's /me endpoint.

// If no email for user, fallback to preferred_username
emails: [
{
value: rawProfile._json.email || rawProfile._json.preferred_username,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I put this in primarily as most of my test users don't have email's (no Office 365) and I wanted to be able to login. Might make more sense to ultimately leave this undefined in the event of no email address.

};
}

async refresh(refreshToken: string, scope: string): Promise<OAuthResponse> {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This currently does not work.

* - https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-permissions-and-consent
* - https://docs.microsoft.com/en-us/graph/permissions-reference
*/
const scope = 'offline_access profile email User.Read';
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Scopes for passport-azure-ad need to be set here.

const useCookieInsteadOfSession = true;
const cookieEncryptionKeys = [
{ key: '12345678901234567890123456789012', iv: '123456789012' },
{ key: 'abcdefghijklmnopqrstuvwxyzabcdef', iv: 'abcdefghijkl' },
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

passport-azure-ad requires express-session or the use of cookies to be sessionless.

@javaniecampbell
Copy link

javaniecampbell commented Aug 20, 2020

@ContrarianChris Can you let me know how I can get access to the information listed in #1967 please so I can build on what you have for the pipelines plugin I am working on please?

You sped up my development a bit faster so a way of accessing that information is critical as well so let me how I can access that information from a plugin e.g. like personal access tokens

@theplatformer
Copy link
Contributor Author

Abandoning this in favour of #2056

@theplatformer theplatformer deleted the azure-aad-auth-provider branch August 21, 2020 04:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants