0


Please or Register to create posts and topics.

External identity server

I have already a running identity server as a micro service and would like to use the existing instance instead of the integrated QuickApp identity service. What is the recommended approach to make this happen?

Since your IdentityServer is different from your API server you'll need to configure a different address for your token endpoint. You'll do that from the file configuration.service.ts 

NB: Changes you'll need to do are highlighted in orange

@Injectable()
export class ConfigurationService
{

    ...
    public baseUrl: string = "http://localhost:5050";
    public tokenUrl: string = "http://youridentityserver.com"; //<---Change here

Then you'll use the newly added configuration in the file endpoint-factory.service.ts

@Injectable()
export class EndpointFactory {
...
private readonly _loginUrl: string = "/connect/token";
    private get loginUrl() { return this.configurations.tokenUrl + this._loginUrl; } //<---Change here

 

Now we need to make the following modifications on the server. From the file Startup.cs

From the method ConfigureServices() delete the call to services.AddIdentityServer() and all related configurations

------Delete start------

// Adds IdentityServer.
services.AddIdentityServer()
// The AddDeveloperSigningCredential extension creates temporary key material for signing tokens.
// This might be useful to get started, but needs to be replaced by some persistent key material for production scenarios.
// See http://docs.identityserver.io/en/release/topics/crypto.html#refcrypto for more information.
.AddDeveloperSigningCredential()
.AddInMemoryPersistedGrants()
// To configure IdentityServer to use EntityFramework (EF) as the storage mechanism for configuration data (rather than using the in-memory implementations),
// see https://identityserver4.readthedocs.io/en/release/quickstarts/8_entity_framework.html
.AddInMemoryIdentityResources(
IdentityServerConfig.GetIdentityResources())
.AddInMemoryApiResources(
IdentityServerConfig.GetApiResources())
.AddInMemoryClients(
IdentityServerConfig.GetClients())
.AddAspNetIdentity<
ApplicationUser>()
.AddProfileService<
ProfileService>();

------Delete end------

Then change these 2 lines to point to your IdentityServer endpoint

.AddIdentityServerAuthentication(options =>
{
        options.Authority = "http://quickapp-pro.ebenmonney.com"; //<---Change here

.AddIdentityServerAuthentication(options =>
{
 options.Authority = "http://localhost:5050/"; //<---Change here

Now from the method Configure() change app.UseIdentityServer(); to app.UseAuthentication();

This will delegate the authentication bit to your chosen IdentityServer instance

Thanks a lot for the hint. Works great!

Hi Eben,

I've tried to use the above approach.  In my case I need to integrate with an SSO identity provider.  There are a couple of differences though.  The SSO does not allow for passing of the UID/PWD, instead it presents a logon dialog to capture this.  Also the method uses GET and passes the params on the QS.

So I have a login endpoint like this

private readonly _loginUrl: string = "/MyWebApi/oauth/authorize?response_type=token&client_id=implicit-MyWebApp&scope=public&redirect_uri=https%3A%2F%2Flocalhost%3A44300%2F&state=";
and in the getLoginEndpoint method I'm using GET and QS hence commented out the params and the POST call

getLoginEndpoint<T>(userName: string, password: string): Observable<T> {

let header = new HttpHeaders({ 'Content-Type': 'application/x-www-form-urlencoded' });

//let params = new HttpParams()
//.append('username', userName)
//.append('password', password)
//.append('client_id', 'implicit-MyWebApp')
//.append('grant_type', 'password')
//.append('scope', 'openid email phone profile offline_access roles MyWebApi);

//return this.http.post<T>(this.loginUrl, params, { headers: header });

return this.http.get<T>(this.loginUrl, { headers: header })
}

The return URL is set there for https://localhost:43000 for local debugging on IIS Express

What is happening is that I am getting JS error and it looks like instead of the external login page opening to capture user credentials, something is instead trying to parse the responses stream (expecting JSON I think) and throwing a parser error.  I think this because the OOB login is expected to pass the user login credentials and handle the response silently.

Any idea what I need to do to handle this scenario.  Is it just a question of navigating to the login url or do I need to make further changes?

Any guidance appreciated

Thanks

Jes

 

 

Hi Eben,

Further to the above.  If I do hit the login url directly in the browser, I get a return url with an access token on the QS

like this

https://localhost:44300/#access_token=%5BJWT_TOKEN%5D&token_type=Bearer&expires_in=7200

Where [JWT_TOKEN] is a well formed base 64 encoded bearer token (see image attached)

So I need to incorporate this flow and handle #access_token QS somehow.

Any pointers would be greatly appreciated

Jes

 

 

Uploaded files:
  • JWT.PNG

Hello Jes,

I believe what you're trying to achieve is to implement OIDC using the Implicit Flow. Emphasis on "Implicit Flow".

QuickApp out of the box implements OAuth2's "Password Grant" which mimics the traditional non-interactive Username/Password way of user authentication, a very popular way of user authentication. The Implicit Flow is also another popular flow/grant for SPAs and using that in QuickApp is among a series of blog articles I'll be doing on this site. But until then there're some good online resources that provides examples of implementing these flows with Angular and IdentityServer.

The standard flows/grants are a specification of OpenID Connect/OAuth2 respectively and the concepts are mostly not vendor specific. Understanding these flows will help you integrate with any compliant vendor easily. They are all based on token transfers and the changes you'll need to make to support another flow will mostly be contained in the "endpoint-factory.service.ts" file.

See the links below for more info on the different grants/flows:

Below are all examples of implementing the Implicit Flow with Angular. These should guide you configure your project correctly for the Implicit Flow:

Jes Kirkup has reacted to this post.
Jes Kirkup

Hello Eben,

Many thanks for your reply.  I've been told that the ID server does not implement OIDC or perhaps it isn't yet exposed (I believe that it is an older implementation of NetIQ access manager).  If it did I think it would be a lot easier.  My only option appears to be opening a page to a login url with all parameters on the URL then handling the redirect.  There isn't any discovery document that I am aware of.

I will take a look at the resources you have kindly provided here and I'll let you know how it goes.

Again thanks for the reply, it is much appreciated.

Jes

 

 

Deleted user has reacted to this post.
Deleted user

Hi Eben,

I just thought that I would update this post as promised and I am happy to report that I have sucessfully integrated the QuickApp template with the corporate SSO solution for my current client (which is based on NetIQ Access Manager).

So in my case there is no need for any of the user/account management functionality or indeed any of the web API functionality provided.  We are using DB first approach with EF Core and mostly generating the DAL/Web API controller methods with the new command line tooling.

Anyway, as has been mentioned earlier in this thread the OOB Password Grant Flow that is implemented in QuickApp cannot be used for my scenario. Also the server does not yet support OIDC (Open ID Connect) further limiting available options – no ability to request a id_token.

With the implicit grant flow the app constructs a URL which is navigated to when logging in.  This redirects to the SSO server which will prompt the user for credentials and then redirect the user back to the app to the requested return URL (allowable redirect URL must be configured for that specific application client id).  There are no client secrets stored in the app on the client.

The redirect back to the client then contains a hash fragment which contains the encoded access token and expiry time.

It looks like this

https://localhost:44300/callback/#access_token=<JWT_TOKEN>&token_type=Bearer&expires_in=7200

Where <JWT_TOKEN> is the base 64 encoded access token as mentioned earlier.  So the challenge was to integrate this flow into the existing QuickApp code base.

After much searching I ended up using a npm package provided by Auth0 called auth0-js

The authorisation parameters are configured in auth-config.ts as follows:

export const AUTH_CONFIG: AuthConfig = {
    CLIENT_ID: 'APPNAME',
    CLIENT_DOMAIN: ' auth.svr.corp.net /AuthorizationServer/APINAME/oauth',
    AUDIENCE: 'ABC', // not used
    REDIRECT: 'https://localhost:44300/',
    SCOPE: 'public',
    LOGOUT: 'https://auth.svr.corp.net/AuthorizationServer/SingleSignOutAndRedirectToReplyUrlWithClientId?clientId=APINAME&returnUrl=https://localhost:44300/',
    APIURL: 'https://localhost:44300/api/',
    REFRESH: 'https://localhost:44300/refresh/'
};

This config is available to the AuthService class and this is where I ended up making nearly all of the changes.

Here is the modified code:

@Injectable()
export class AuthService {
    // Start of changes to handle OAUTH Implicit Grant
    // This property is set in app component constructor if an auth hash is present on the query string
    // It is needed because the router logic sets the URL to lowercase which invalidates the JWT token

    public hashfragment: string;
    // Create the Auth0 web auth instance
    auth0 = newauth0.WebAuth({
        clientID: AUTH_CONFIG.CLIENT_ID,
        domain: AUTH_CONFIG.CLIENT_DOMAIN,
        responseType: 'token',
        redirectUri: AUTH_CONFIG.REDIRECT,
        audience: AUTH_CONFIG.AUDIENCE,
        scope: AUTH_CONFIG.SCOPE
    });
    userProfile: any;

    GSFlogin() {
        this.logoutRedirectUrl = AUTH_CONFIG.LOGOUT;
        this.auth0.baseOptions.redirectUri = AUTH_CONFIG.REDIRECT;
        if (this.isLoggedIn) this.logout();
        this.auth0.authorize();
    }

    GSFRefresh() {
        this.auth0.baseOptions.redirectUri = AUTH_CONFIG.REFRESH;
        this.renewToken();
    }

    GSFRefreshCallback() {
        // This function is called in the ngOnInit() event of the authrefresh component
        // This occurs when the /refresh URL is processed in a hidden IFRAME
        // The auth token is renewed in the background without losing current context
        // Ignore the auth request if the GSF redirect hash is not present on the URL
        if (!window.location.href.match(/access_token=(.*)/)) return;
        // Revert URL to stored mixed case value as the hash gets lowercased
        window.location.hash = this.hashfragment;
        this.auth0.parseHash(this.hashfragment, (err, authResult) => {
            if (authResult && authResult.accessToken) {
                window.location.hash = '';
                // Update the stored access token and expiry time
                this.processGSFLoginResponse(authResult, true);
            } else if (err) {
                console.error(`Refresh Error: ${err.error}`);
                //alert(`GSF Refresh Error: ${err.error}`);
            }
        });
    }

    public renewToken() {
        this.auth0.checkSession({}, (err, result) => {
            if (err) {
                console.log(`renewToken: Could not get a new token (${err.error}: ${err.error_description}).`);
            } else {
                console.log(`renewToken: Successfully renewed auth!`);
            }
        });
    }

    handleAuth() {
        // This function is called in the ngOnInit() event of the callback component
        // when the callback URL is processed
        // Ignore the auth request if the GSF redirect hash is not present on the URL
        if (!window.location.href.match(/access_token=(.*)/)) return;

        // Revert URL to stored mixed case value as the hash gets lowercased
        window.location.hash = this.hashfragment;
        this.auth0.parseHash(this.hashfragment, (err, authResult) => {
            if (authResult && authResult.accessToken) {
                window.location.hash = '';
                this.processGSFLoginResponse(authResult, true);
            } else if (err) {
                console.error(`Error: ${err.error}`);
                alert(`GSF Error: ${err.error}`);
                this.logout();
            }
            this.router.navigate(['/']);
        });
    }
    private processGSFLoginResponse(authResult: any, rememberMe: boolean) {
        let accessToken = authResult.accessToken;
        if (accessToken == null) {
            throw new Error("Received accessToken was empty");
        }

        let refreshToken = "";
        let expiresIn = authResult.expiresIn;
        let tokenExpiryDate = newDate();
        tokenExpiryDate.setSeconds(tokenExpiryDate.getSeconds() + expiresIn);
        let accessTokenExpiry = tokenExpiryDate;
        let jwtHelper = newJwtHelper();
        let decodedAccessToken = <AccessToken>jwtHelper.decodeToken(authResult.accessToken);
        // TODO Replace hardcoded permissions with DB read or ARBT roles
        letpermissions: PermissionValues[] = ["roles.view", "users.view", "users.manage", "users.view", "users.manage", "roles.view", "roles.manage", "roles.assign"]
        // Extract user claims from GSF access token
        decodedAccessToken.name = decodedAccessToken["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname"];
        decodedAccessToken.fullname = decodedAccessToken["http://schemas.arbt.xxx.com/displayname"];
        decodedAccessToken.email = decodedAccessToken["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"];
        if (!this.isLoggedIn) {
            this.configurations.import(decodedAccessToken.configuration);
        }
        let user = newUser(
            decodedAccessToken.sub,
            decodedAccessToken.name,
            decodedAccessToken.fullname,
            decodedAccessToken.email,
            decodedAccessToken.jobtitle,
            decodedAccessToken.phone_number,
            Array.isArray(decodedAccessToken.role) ? decodedAccessToken.role : [decodedAccessToken.role]);
        user.isEnabled = true;
        this.saveUserDetails(user, permissions, accessToken, refreshToken, accessTokenExpiry, rememberMe);
        this.reevaluateLoginStatus(user);
        returnuser;
    }

// End of changes to handle OAUTH Implicit Grant

There is a method to replace the default login method and this configures and calls the auth0-js authorize method. The callback URL has a route defined to a component “callback” and this simply calls into the library again the “handleAuth” method.

From here a slight adapted version of process response method is used to decode and parse the token.  There is still a little work with roles to do here but I am probably going to be handling that all inside the web API. There is also an option to configure course grained roles via the identity server. I am just hard coding them at the moment.

One area that was a little tricky to deal with was providing a “silent refresh” mechanism.  The OOB solution uses refresh tokens which are not available for implicit grant.  The way things work with the SSO server is that you are issued with an access token that is valid for a period of time, in my case 2 hours. If you hit the URL again within that timeframe the auth server will respond with a new token on the redirect URL without any login required.

So it is necessary to hit the auth server to keep alive the app session.  Thankfully the auth0 library handles all of the plumbing here.  Creation of hidden IFRAME and usage of postmessage API to grab the access token.

I use a timer to check every 5 minutes the expiry date of the access token in local storage.  If we are within 10 minutes of timing out then a silent refresh is performed and we are good for another couple of hours. I made some mods to the login form to remove the user/password with some blurb and a button to initiate the implicit grant flow.

Its all working well.

Thanks for your help.

Kind regards

Jes

 

Uploaded files:
  • p1.png
  • p2.png
  • p3.png
  • p4.png
  • p5.png

Great progress.
And thanks for sharing your approach. Its a good reference for people wanting to do similar.

Cheers.