Getting started

This tutorial will take you through the password credential flow of our application. We also have to specify access to a specific company using the CompanyKey header parameter.

Before you can start you have to contact us on www.unimicro.no/integrasjonspartnere, so that we can create a client id for you. To be able to create a client for you, we need to know which kind of application you are working on, and a redirect url that we can redirect to.

Different auth flows

We support mulitple authentication flows, and here we will give a quick introduction to these scenarios:

In addition to these scenarios you also need to consider if your application should be single tenant or multi tenant.




Web site (Auth code flow)


Step 1 - Redirect to the log in

<a href="https://test-login.unieconomy.no/connect/authorize?
scope=openid%20profile%20AppFramework%20offline_access
&response_type=code
&client_id=d2cc9a0e-4515-4a6c-ad05-2487873d777b
&redirect_uri=https://integration-partner/post-login">

Note: Scopes relevant to the client should be listed in scope, delimited with space.

Now the user will be shown the login screen and will log in and give consent. The user will allso select which company to log into

Login


Consent



If the client is single tenant, the user will be shown a company selector

Select company

Step 2 - Exchange auth code with access code

test-login.unieconomy.no will redirect to the provided redirect-url, and will provide a code as an url parameter https://integration-partner/post-login?code={auth_code}

You then have to invoke the Token end-point whit the provided auth code.

POST https://dev-ueidentity.azurewebsites.net/connect/token
 
Headers:
"Content-Type":"application/x-www-form-urlencoded"
 
Body:
"grant_type": "authorization_code",
"code": CODE_FROM_REDIRECT,
"redirect_uri": https://integration-partner/post-login,
"client_id": 4a768513-705e-49e9-af2a-4a16a3924117

Example in C#:

var httpClient = new HttpClient();

var pairs = new List<KeyValuePair<string, string>>
{
    new KeyValuePair<string, string>("client_id", "d2cc9a0e-4515-4a6c-ad05-2487873d777b"),
    new KeyValuePair<string, string>("code", code),
    new KeyValuePair<string, string>("grant_type", "authorization_code"),
    new KeyValuePair<string, string>("client_secret", "wZK7bGkXLEKbVXBg+Ds4Og=="),
    new KeyValuePair<string, string>("redirect_uri", "https://integration-partner/post-login")
};

var content = new FormUrlEncodedContent(pairs);
var result = await httpClient.PostAsync(new Uri("https://test-login.unieconomy.no/connect/token"), content)

You’ll then get a JSON response that looks like this:

{
	"id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6Im...",
	"access_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6ImVhMD...",
	"expires_in": 3600,
	"token_type": "Bearer",
	"refresh_token": "8942647edb873533c35a..."
}

Step 2b - Exchange refresh code with a new access token

The process is simular to the original one, the POST requst have different form values, but the reponse is the same.

var pairs = new List<KeyValuePair<string, string>>
{
    new KeyValuePair<string, string>("client_id", "d2cc9a0e-4515-4a6c-ad05-2487873d777b"),
    new KeyValuePair<string, string>("refresh_token", identityResponse.refresh_token),
    new KeyValuePair<string, string>("grant_type", "refresh_token"),
    new KeyValuePair<string, string>("client_secret", "wZK7bGkXLEKbVXBg+Ds4Og=="),
    new KeyValuePair<string, string>("redirect_uri", "https://integration-partner/post-login")
};


Step 3 - Contact the UniEconomy API

Now you have an access token and are ready to contact the API. If you have a single tenant client and the user has selected a company you don’t specify a CompanyKey in the headers, but if you have the possiblity to change company you must provide the CompanyKey header.

Single tenant request:

httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);

var apiResult = await httpClient.GetAsync(new Uri("https://test-api.unieconomy.no/api/biz/companysettings/1?select=companyname,organizationnumber"));

Multi tenant request:

httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
httpClient.DefaultRequestHeaders.Add("CompanyKey", "015eb513-753a-4942-9f6a-8ba930e33dc6");

var apiResult = await httpClient.GetAsync(new Uri("https://test-api.unieconomy.no/api/biz/user?action=current-session"));

In both cases your response should look like:

{
    "Permissions": [],
    "License": {},
    "DisplayName": "My Name",
    "Email": "dev@post.no",
    "GlobalIdentity": "ee5a6e9f-9ca8-4c97-9bf0-6563633d594f",
    "PhoneNumber": "",
    "LastLogin": "2019-02-12T12:15:23.85Z",
    "UserName": "Username",
    "Protected": false,
    "BankIntegrationUserName": null,
    "IsAutobankAdmin": false,
    "StatusCode": null,
    "CustomValues": null,
    "ID": 1,
    "Deleted": false,
    "CreatedAt": null,
    "UpdatedAt": null,
    "CreatedBy": null,
    "UpdatedBy": null
}

Step 3b - Multi tenant request company key

Purpose: Returns a list of all available companies for your current logged in user. The “CompanyKey” is a mandatory header param for all API requests

GET https://test-api.unieconomy.no/api/init/companies

Headers

Content-Type: application/json
Authorization: Bearer [JWT token]

Response You should now get a list of your companies.

{
    "Name":"MySales AS",
    "Key":"015eb513-753a-4942-9f6a-8ba930e33dc6",
    "WebHookSubscriberId":null,
    "IsTest":false,
    "FileFlowEmail":null,
    "ID":5,
    "Deleted":false,
    "CreatedAt":"2017-02-01T15:55:43.46Z",
    "UpdatedAt":null,
    "CreatedBy":null,
    "UpdatedBy":null
}




SPA application

For SPA applications we recommend using Implicit flow with silent refresh / sliding-window. This short introduction will use a library called oidc-client-js and Angular.

Pre-requisits


Step 1 - Login component

import { Component, OnInit } from '@angular/core';
import { AuthService } from '../../core/services/auth.service';

@Component({
  selector: 'app-login-with-ids',
  templateUrl: './login-with-ids.component.html',
  styleUrls: ['./login-with-ids.component.scss']
})
export class LoginWithIdsComponent implements OnInit {

  constructor(private _authService: AuthService) { }


  signin() {
    this._authService.startSignin();
  }
  ngOnInit() {
  }

}

Here we see that we import a service called AuthService, and this is the next step.

We would also create a component for unauthorized

import { Component, OnInit } from '@angular/core';

@Component({
  selector: 'app-unauthorized',
  templateUrl: './unauthorized.component.html',
  styleUrls: ['./unauthorized.component.css']
})
export class UnauthorizedComponent implements OnInit {

  constructor() { }

  ngOnInit() {
  }

}

Step 2 - AuthService

import { Injectable, EventEmitter } from '@angular/core';
import { Http, Headers, RequestOptions, Response } from '@angular/http';
import { Observable } from 'rxjs/Rx';
import { environment } from '../../../environments/environment';
import { UserManager, Log, MetadataService, User } from 'oidc-client';

const settings: any = {
  authority: environment.authority,
      client_id: environment.client_id,
      redirect_uri: environment.redirect_uri,
      post_logout_redirect_uri: environment.post_logout_redirect_uri,
      response_type: environment.response_type,
      scope: environment.scope,
      filterProtocolClaims: environment.filterProtocolClaims,
      loadUserInfo: environment.loadUserInfo,
      automaticSilentRenew: true,
      silent_redirect_uri: environment.silent_redirect_uri,

};

@Injectable({
  providedIn: 'root',
})
export class AuthService {
  mgr: UserManager = new UserManager(settings);
  userLoadededEvent: EventEmitter<User> = new EventEmitter<User>();
  currentUser: User;
  loggedIn = false;

  authHeaders: Headers;

  constructor(private http: Http) {
    this.mgr
      .getUser()
      .then(user => {
        if (user && !user.expired) {
          this.loggedIn = true;
          this.currentUser = user;
          this.userLoadededEvent.emit(user);
        } else {
          this.loggedIn = false;
        }
      })
      .catch(err => {
        this.loggedIn = false;
      });
    this.mgr.events.addSilentRenewError(function(res) {
      console.log(res);
    });

    this.mgr.events.addUserLoaded(() => {
      this.mgr.getUser().then(user => {
        this.currentUser = user;
      });
    });

  }

  getToken(): string {
    return this.currentUser != null ?  this.currentUser.access_token : null;
  }

  getUser() {
    this.mgr
      .getUser()
      .then(user => {
        console.log('got user', user);
        this.currentUser = user;
        this.userLoadededEvent.emit(user);
      })
      .catch(function(err) {
        console.log(err);
      });
  }

  isLoggedInObs(): Observable<boolean> {
    return Observable.fromPromise(this.mgr.getUser()).map<User, boolean>(
      user => {
        if (user && !user.expired) {
          return true;
        } else {
          return false;
        }
      }
    );
  }

  startSignin() {
    this.mgr
      .signinRedirect({ data: 'some data' })
      .then(function() {
        console.log('signinRedirect done');
      })
      .catch(function(err) {
        console.log(err);
      });
  }

  logOut(): Promise<any> {
    return this.mgr.signoutRedirect();
  }

}

Here we see that we need some environment settings:

export const environment = {
  production: false,

  base_url: 'https://test-api.unieconomy.no/api/',
  authority: 'https://test-login.unieconomy.no:59454',
  client_id: 'a5fc5fb6-54ba-454f-b41c-443390c3675f',
  redirect_uri: 'https://integration-partner/assets/auth.html',
  post_logout_redirect_uri: 'https://integration-partner/#/login',
  silent_redirect_uri: 'https://integration-partner/assets/silent-renew.html',
  automaticSilentRenew: true,
  response_type: 'id_token token',
  scope: 'openid profile AppFramework',
  filterProtocolClaims: true, // prevents protocol level claims such as nbf, iss, at_hash, and nonce from being extracted from the identity token as profile data
  loadUserInfo: true
};

You may notice that we here suggest to use an html for “post-login-redirect” and “slient-redirect-uri”. This is to avoid loading the angular app.


Step 3 - Redirect htmls

auht.html

<script src="https://cdnjs.cloudflare.com/ajax/libs/oidc-client/1.6.1/oidc-client.js"></script>
<script>

  var mgr = new Oidc.UserManager();
  mgr.signinRedirectCallback().then(() => {
        window.history.replaceState({},
            window.document.title,
            window.location.origin);
        window.location = "/dashboard";
    }, error => {
        console.error(error);
    });

</script>

silent-renew.html

<script src="https://cdnjs.cloudflare.com/ajax/libs/oidc-client/1.6.1/oidc-client.js"></script>
<script>

new Oidc.UserManager().signinSilentCallback()
        .catch((err) => {
            console.log(err);
        });

</script>

Step 4 - JWT Interceptor

import { Injectable } from '@angular/core';
import { HttpInterceptor, HttpEvent, HttpHandler, HttpRequest, HttpHeaders } from '@angular/common/http';
import { Observable } from 'rxjs';
import { AuthService } from './auth.service';

// Interceptor for add JWT token to each and every http request goin out form the app to API
@Injectable({providedIn: 'root'})
export class JwtInterceptor implements HttpInterceptor {
  constructor(private authenticationService: AuthService) {}

  intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
      const token = this.authenticationService.getToken();
      if (token) {
          request = request.clone({
              setHeaders: {
                Authorization: `Bearer ${token}`,
                'Content-Type' : 'application/json'
              }
          });
      }

      return next.handle(request);
  }
}

We allso want to add a guard service to protect our resources

import { Injectable } from '@angular/core';
import {
  CanActivate,
  Router,
  RouterStateSnapshot,
  ActivatedRouteSnapshot
} from '@angular/router';
import { AuthService } from './auth.service';

@Injectable({
  providedIn: 'root',
})
export class AuthGuardService implements CanActivate {
  constructor(private idpsService: AuthService, private router: Router) {}

  canActivate(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ) {
    const isLoggedIn = this.idpsService.isLoggedInObs();
    isLoggedIn.subscribe(loggedin => {
      if (!loggedin) {
        this.router.navigate(['/login'], {
          queryParams: { returnUrl: state.url }
        });
      }
      if (!this.idpsService.IsAdminUser()) {
        this.router.navigate(['/unAuthorized'], {
          queryParams: { returnUrl: state.url }
        });
      }
    });

    return isLoggedIn;
  }
}

Step 5 - Register the services

app.routing.ts

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { AdminLayoutComponent } from './layouts/admin-layout/admin-layout.component';
import { AuthGuardService } from './core/services/auth-guard.service';
import { LoginWithIdsComponent } from './auth/login-with-ids/login-with-ids.component';
import { UnauthorizedComponent } from './auth/unauthorized/unauthorized.component';

const routes: Routes = [
  { path: 'login', component: LoginWithIdsComponent },
  {
    path: 'unAuthorized',
    component: UnauthorizedComponent
  },
  {
    path: '',
    component: AdminLayoutComponent,
    children: [
      {
        path: 'clients',
        loadChildren: 'app/clients/clients.module#ClientsModule',
        canActivate: [AuthGuardService]
      },
      {
        path: '**',
        redirectTo: 'clients',
        pathMatch: 'full'
      }
    ]
  }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule {}

app.module.ts

import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { NgModule } from '@angular/core';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { BrowserModule } from '@angular/platform-browser';
import { LocationStrategy, HashLocationStrategy } from '@angular/common';
import { AppRoutingModule } from './app.routing';
import { SharedModule } from './shared/shared.module';
import { AppComponent } from './app.component';
import { AdminLayoutComponent } from './layouts/admin-layout/admin-layout.component';
import { AuthService } from './core/services/auth.service';
import { AuthGuardService } from './core/services/auth-guard.service';
import { CoreModule } from './core/core.module';
import { AuthModule } from './auth/auth.module';


@NgModule({
  imports: [
    BrowserAnimationsModule,
    SharedModule,
    CoreModule,
    AuthModule,
    AppRoutingModule,
    BrowserModule,

    NgbModule.forRoot()
  ],
  declarations: [
    AppComponent,
    AdminLayoutComponent,
  ],
  providers: [
    AuthService, AuthGuardService, { provide: LocationStrategy, useClass: HashLocationStrategy },

  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

core.module.ts:

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FooterComponent } from './footer/footer.component';
import { NavbarComponent } from './navbar/navbar.component';
import { SidebarComponent } from './sidebar/sidebar.component';
import { ErrorInterceptor } from './services/error.interceptor';
import { JwtInterceptor } from './services/jwt.interceptor';
import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { SharedModule } from '../shared/shared.module';

@NgModule({
  declarations: [
    FooterComponent,
    NavbarComponent,
    SidebarComponent
  ],
  imports: [
    CommonModule,
    SharedModule
  ],
  exports: [
    FooterComponent,
    NavbarComponent,
    SidebarComponent
  ],
  providers: [
    { provide: HTTP_INTERCEPTORS, useClass: JwtInterceptor, multi: true },
    { provide: HTTP_INTERCEPTORS, useClass: ErrorInterceptor, multi: true },
  ]

})
export class CoreModule { }