Salesforce Authentication (Auth2.0 Webflow)

Flask Salesforce Oauth



Now it's time to join our app with Salesforce. To send calls to Salesforce API we need OAuth2.0 Access Token and I plan to implement Webflow to get it. Also Access Token has short life and to have access to the API we need to refresh it periodically with refresh token. All this should be setup properly in connected app and in oauth handshake process.

First of all we need Connected App with its Key and Secret. Create new Developer Org (it's free) and create Connected App same in this screenshot

Salesforce Connected App for Flask project

Consumer Key and Consumer Secret should be stored as ENV Variables for later use in our project

...
app.config['SFDC_CONNECTED_APP_KEY'] = os.environ['SFDC_CONNECTED_APP_KEY']
app.config['SFDC_CONNECTED_APP_SECRET'] = os.environ['SFDC_CONNECTED_APP_SECRET']
...

OAuth Handhake contains 2 steps:
- first you need to redirect user from your app to salesforce login page (Frontend part).
- next catch redirect from Salesforce with code and state information to get from Salesforce access token and refresh token (Backend part)

Step #1

To redirect user to the Salesforce Oauth Login page we need to know what type of org user tries to connect. It depends will it be https://login.salesforce.com or https://test.salesforce.com (at this moment I simplify it a little bit and ignore orgs with custom domains and disabled standard login pages)

It can be simple modal with select

Select Salesforce Org Type for Oauth

I will not post the code here as it is pretty simple modal we already did before with our SLDS-Modal. One new thing here is SLDS-Select UI Component I've created (same as SLDS-Input) but it will be described later.

And when user click Connect all required information comes to sfdc.service.ts (new service create with ng cli).

import { Injectable } from '@angular/core';
import { RemoteService } from './remote.service';

@Injectable({
    providedIn: 'root'
})
export class SfdcService {

    constructor(
        private remoteService: RemoteService
    ) { }

    startSalesforceOAuth(task, orgType, connectedAppKey) {

        const loginBaseUrl = orgType === 'sandbox' ? 'https://test.salesforce.com' : 'https://login.salesforce.com';
        const baseUrl = window.location.protocol +
                        '//' +
                        window.location.hostname +
                        (window.location.port ? ':' + window.location.port : '');
        const redirectUrl = baseUrl + '/sfdc/oauth/callback';

        const stateObj = {
            taskId: task.id,
            loginBaseUrl,
            redirectUrl
        };

        const path = `/services/oauth2/authorize?response_type=code&client_id=${encodeURIComponent(connectedAppKey)}` +
                     `&redirect_uri=${encodeURIComponent(redirectUrl)}&state=${btoa(JSON.stringify(stateObj))}` +
                     `&scope=api+refresh_token`;

        top.location.href = loginBaseUrl + path;

    }

}
Interesting thing here is stateObj where we store required for Step #2 information. It is JSON serialized and Base64 encoded string added as GET parameter to the Salesforce login url. After successful login salesforce will send state variable back to our oauth callback url.

Step #2 
After Login in Salesforce side user should be redirected to our callback url. We set it in redirect_uri variable in Ster #1 and also specified it in Connected App earlier.

import base64
import json
import requests
from urllib.parse import urlencode, quote_plus
from flask import Blueprint, request, current_app, redirect, abort, jsonify
from flask_login import login_required, current_user
from models import db, Task

sfdc_bp = Blueprint('sfdc', __name__, url_prefix='/sfdc')

@sfdc_bp.route('/oauth/callback', methods=['GET'])
@login_required
def oauth_handshake():

    # Request Example: 
    # /sfdc/oauth/callback?code=aPrx9pB8PA1X2QOXBj2XFIklPku.mPE7TzVdMCdcxfaw6K0Wnd5K3iiUoG_vGiqp82EBNFpXxQ%3D%3D&state=eyJ0YXNrSWQiOjIxLCJsb2dpbkJhc2VVcmwiOiJodHRwczovL2xvZ2luLnNhbGVzZm9yY2UuY29tIn0%3D

    code = request.args.get('code')
    state = json.loads(base64.b64decode(request.args.get('state')).decode('utf-8'))

    task: Task = (Task.query.filter(Task.id == state['taskId'],
                                    Task.user_id == current_user.id)
                            .first_or_404())

    headers = {
        'Content-Type': 'application/x-www-form-urlencoded',
        'Accept': 'application/json',
    }
    payload = {
        'grant_type': 'authorization_code',
        'code': code,
        'client_id': current_app.config['SFDC_CONNECTED_APP_KEY'],
        'client_secret': current_app.config['SFDC_CONNECTED_APP_SECRET'],
        'redirect_uri': state['redirectUrl']
    }
    req_body = urlencode(payload, quote_via=quote_plus)

    resp = requests.post(state['loginBaseUrl'] + '/services/oauth2/token',
                         headers=headers,
                         data=req_body)

    token_results = resp.json()
    '''
    token_results Example:
    {
        'access_token': '00Dxxxxxxxxxx',
        'refresh_token': '5Aezzzzzzzzzz',
        'signature': '8KFGb34p3vMLhSRHHmJZN3Bux9tlcA1+cvgIACw2SG4=',
        'scope': 'refresh_token api',
        'instance_url': 'https://ap17.salesforce.com',
        'id': 'https://login.salesforce.com/id/00D2x000003v71DEAQ/0052x000001S9ojAAC',
        'token_type': 'Bearer',
        'issued_at': '1589799677876'
    },
    {
        'error': 'invalid_grant',
        'error_description': 'expired authorization code'
    }
    '''

    if 'access_token' not in token_results:
        abort(500, json.dumps(token_results))

    task.sfdc = token_results

    db.session.commit()

    return redirect('/#/tasks/' + str(task.id))
All looks pretty simple here. All we need is to receive code value and decode our state to build new request to Salesforce API for access_token and refresh_token. Example of the Salesforce response you can find in the code. I save whole response to the task in DB for latter use as it also contains other useful information.

If Step #1 and Step #2 finishes successfully page with Task should be reloaded and behave depending on new information (in screenshots you can see raw text output of the Task.sfdc field).

One two important things we need to implement is Connection Validation and Refresh Token. I will describe it in next article.

Comments

Popular posts from this blog

Salesforce Authentication 2 (Token Validation and Refresh)

HTTPS in local environment for Angular + Flask project.

Salesforce Lightning Design System (SLDS)