Back to the blog
Technology

How to implement OAuth SSO in Angular & Flask Application?

author image

Nilabjo

Date posted: July 03, 2020

It is not an easy task providing seamless authentication experience across all your web applications. A centralized point of authentication is now the preferred method. There are multiple ways to achieve this. In this article, I have shared the lessons I learnt from implementing Angular Flask based SSO for one of our enterprise clients.

Single sign-on is an authentication scheme where a user can log in with one set of username and password across multiple applications. There can be a single point of authentication via any Identity Access Manager (IAM) of your choice.

For this client we choose to use openIAM and to communicate with the IAM server we used OAuth protocol. To obtain a token from the IAM server we used Authorization Code Grant method.

OAuth Flow

Every application in the array of applications interacts with their respective services which are decoupled from each other. Since the Authorization Code grant has the extra step of exchanging the authorization code for the access token, it provides an additional layer of security compared to the Implicit Grant type.

image1

The client sends a request to the authorization server(a flask app in this case) and that application makes calls to the OpenIAM server to obtain the pertaining authorization token and then set cookies on the client-side.

SSO Implementation

The following diagram represents our execution of SSO. You can see how the user interacts with the client app and how the Flask app talks to the OpenIAM server and returns with a valid token for the user.

image2

OAuth with OpenIAM in Flask App Implementation

Before moving on with the implementation, we added a redirect URL to the client’s IAM server. That URL was passed along with other properties when the first redirect to the consent page happens.

After the user successfully logged in, the IAM application redirected the user to the redirect URL mentioned above.

Hashout’s approach

Create an endpoint (/login) in the flask application

@ns.route('/login')
class Login(Resource):
    def get(self):
        state = uuid.uuid4()
        nonce = uuid.uuid4()
        url = '{base_url}?&client_id={client_id}&redirect_uri={redirect_uri}&response_type=code&scope=openid&state={state}&nonce={nonce}'.format(base_url = self.config['authorization_base_url'], client_id = self.config['client_id'], redirect_uri = self.config['redirect_uri'], state = state, nonce = nonce)

        # State is used to prevent CSRF, keep this for later.
        # store state in db
        openiam_session = OpenIAMSession()
        openiam_session.isactive = 1
        openiam_session.state = str(state)
        openiam_session.nonce = str(nonce)
        db.session.add(openiam_session)
        db.session.commit()
        # session['open_iam_state'] = str(state)
        return redirect(url)

Query param client_id should be available in the openIAM settings. State and nonce params are used to verify if the request was the same one when the authentication process was initiated.

When deploying the flask app via gunicorn, there can be multiple instances of the same app, and using a localized app session won’t persist over different requests to the app. Hence, the state param that you just created and passed won’t exist when openIAM redirects you back to the flask app. This is why we use a database to manage the session instead of using a flask session.

Create the redirect endpoint

# Redirect uri
@ns.route('/openiam')
class OpenIAMCallback(Resource):
    def get(self):
        # Access code from the query params
        # Use access code to retrieve openiam token
        # Exchange openiam token with portal token
        state = request.args.get('state')
        code = request.args.get('code')

        # Create the response, we'll set cookies later
        response = make_response(redirect('{}'.format(ANGULAR_APP_DOMAIN)))

        db_state = db.session.query(OpenIAMSession).filter(OpenIAMSession.state == state).filter(OpenIAMSession.isactive == 1).one()

        if db_state is not None:
            db_state.isactive = 0
            db.session.add(db_state)
            db.session.commit()
            payload = {
                'client_id': self.config['client_id'],
                'client_secret': self.config['client_secret'],
                'grant_type': 'authorization_code',
                'redirect_uri': self.config['redirect_uri'],
                'code': code
            }

            headers = {
                'Content-Type': 'application/x-www-form-urlencoded'
            }

            resp = requests.post(‘<openiam-serve>/idp/oauth2/token’, data=payload, headers=headers, verify=settings.VERIFY_SSL)
            code = resp.status_code
            resp = resp.json()
            create_session_data = {
                'email': email_response['email'],
                ‘Openiam_token’: resp[‘access_token’]
            }

            # JWT token
            service_session =    account_session.sso_create_session(create_session_data)

            cookies = {
                TOKEN: service_session['access_token']
            }
            response = set_custom_cookie(response, **cookies)

*The above URL must be registered with the IAM server.

After the IAM server redirects you back to the flask app, you can check for consistency of the state value and proceed to make a REST call to obtain the openIAM token. You might need a param called client secret to make this call, which should be available with the IAM provider.

Although the user will be authenticated after signing in with OpenIAM, every application will have its own JWT token issued by its respective authorization server(flask app). In the end, we set a cookie called TOKEN with the response to the front end.

How Angular app handles single sign-on

After the flask app redirects the user back to the angular app, angular guards will check for the cookie which contains the token generated in the previous step.

export class RouterGuardService implements CanActivate {
  public staticHelperClass = HelperClass;

  constructor(
    private cookieService: CookieService,
    private constantService: ConstantService,
    private router: Router
  ) {}

  canActivate(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ): boolean {
    const ssoEnabled = this.constantService.SSO_ENABLED;

    if (
      (!this.cookieService.get(this.constantService.TOKEN_NAME)) &&
      ssoEnabled
    ) {
      window.location.href =
        this.constantService.IAM_DOMAIN + '/complete/login';
      return false;
    }

    if (this.cookieService.get(this.constantService.TOKEN_NAME)) {
      const decodedToken = this.staticHelperClass.tokenDecoder(
        this.cookieService.get(this.constantService.TOKEN_NAME)
      );
      if (!decodedToken.identity.role) {
        this.router.navigateByUrl('/authentication/login');
        return false;
      }
    }

    return true;
  }
}

There is a business logic in the above code. The token generated initially is used to generate a subsequent token with roles and environment details. It doesn’t pertain to the single sign-on feature.

After logging into the angular application, the user will see a common header component, which will decode the IAM token from the cookie and display its user information. This component will also handle logout and update the user details in the IAM server. This web component will be used across all the applications where single sign-on is implemented.

Toggle single sign-on from the database

A toggle switch is handy in case you have to switch back to legacy login. We can implement this using a new table that contains key-value pairs of any application-level configuration.

The angular application keeps track of single sign-on in a global constant variable called SSO_ENABLED. When the application initially loads the app initializer hook in angular is executed which checks if SSO is enabled or not.

export class AppInitService {

  constructor(
    private http: HttpClient,
    private cookieService: CookieService,
    private constantService: ConstantService,
  ) {
  }

  Init() {

    return this.http
      .get<SSOEnableResponse>(this.constantService.getUrl(this.constantService.SSO_ENABLED_URL))
      .toPromise()
      .then((response) => {
        this.constantService.SSO_ENABLED = response.sso_enabled;
      });
  }

}

Here, the angular app checks if the SSO is enabled on the service by making a call to the flask application. A simple endpoint on the backend would suffice to make this trivial check.

Result

Voila, yet another happy client and a seamless customer experience achieved through cutting edge combination of technology and best practices. There are multiple ways to achieve this and you can use a framework of your choice or Access Managers.

Browse all categories