- Introduction
- Prerequisites
- Quick Start
- Front End
- Back End
- Set User Session
- Logout
- Conclusion
- Support
- License
This tutorial will demonstrate how to use OAuth 2.0 and OpenID Connect to add authentication to a Python/Flask application.
Users are redirected to your Okta organization for authentication.
After logging into your Okta organization, an authorization code is returned in a callback URL. This authorization code is then exchanged for an id_token.
The Okta Sign-In Widget is a fully customizable login experience. You can change how the widget looks with CSS and is configured with JavaScript.
This custom-branded login experience uses the Okta Sign-In Widget to perform authentication, returning an authorization code that is then exchanged for an id_token.
This sample app depends on Node.js for front-end dependencies and some build scripts - if you don't have it, install it from nodejs.org.
# Verify that node is installed
$ node -vThen, clone this sample from GitHub and install the front-end dependencies:
# Clone the repo and navigate to the samples-python-flask dir
$ git clone git@github.com:okta/samples-python-flask.git && cd samples-python-flask
# Install the front-end dependencies
[samples-python-flask]$ npm installsource venv/bin/activate
pip install -r requirements.txtStart the back-end for your sample application with npm start or python app.py. This will start the app server on http://localhost:3000.
By default, this application uses a mock authorization server which responds to API requests like a configured Okta org - it's useful if you haven't yet set up OpenID Connect but would still like to try this sample.
To start the mock server, run the following in a second terminal window:
# Starts the mock Okta server at http://127.0.0.1:7777
[samples-python-flask]$ npm run mock-oktaIf you'd like to test this sample against your own Okta org, navigate to the Okta Developer Dashboard and follow these steps:
- Create a new Web application by selecting Create New Application from the Applications page.
- After accepting the default configuration, select Create Application to redirect back to the General Settings of your application.
- Copy the Client ID and Client Secret, as it will be needed for the client configuration.
- Finally, navigate to
https://{yourOktaDomain}.com/oauth2/defaultto see if the Default Authorization Server is setup. If not, let us know.
Then, replace the oidc settings in .samples.config.json to point to your new app:
// .samples.config.json
{
"oidc": {
"oktaUrl": "https://{{yourOktaDomain}}.com",
"issuer": "https://{{yourOktaDomain}}.com/oauth2/default",
"clientId": "{{yourClientId}}",
"clientSecret": "{{yourClientSecret}}",
"redirectUri": "http://localhost:3000/authorization-code/callback"
}
}When you start this sample, the AngularJS 1.x UI is copied into the dist/ directory. More information about the AngularJS controllers and views are available in the AngularJS project repository.
With AngularJS, we include the template directive ng-click to begin the login process. When the link is clicked, it calls the login() function defined in login-redirect.controller.js. Let’s take a look at how the OktaAuth object is created.
// login-redirect.controller.js
class LoginRedirectController {
constructor(config) {
this.config = config;
}
$onInit() {
this.authClient = new OktaAuth({
url: this.config.oktaUrl,
issuer: this.config.issuer,
clientId: this.config.clientId,
redirectUri: this.config.redirectUri,
scopes: ['openid', 'email', 'profile'],
});
}
login() {
this.authClient.token.getWithRedirect({ responseType: 'code' });
}
}There are a number of different ways to construct the login redirect URL.
- Build the URL manually
- Use an OpenID Connect / OAuth 2.0 middleware library
- Use AuthJS
In this sample, we use AuthJS to create the URL and perform the redirect. An OktaAuth object is instantiated with the configuration in .samples.config.json. When the login() function is called from the view, it calls the /authorize endpoint to start the Authorization Code Flow.
You can read more about the OktaAuth configuration options here: OpenID Connect with Okta AuthJS SDK.
Important: When the authorization code is exchanged for an access_token and/or id_token, the tokens must be validated. We'll cover that in a bit.
To render the Okta Sign-In Widget, include a container element on the page for the widget to attach to:
<!-- overview.mustache -->
<div id="sign-in-container"></div>Then, initialize the widget with the OIDC configuration options:
// login-custom.controller.js
class LoginCustomController {
constructor(config) {
this.config = config;
}
$onInit() {
const signIn = new SignIn({
baseUrl: this.config.oktaUrl,
clientId: this.config.clientId,
redirectUri: this.config.redirectUri,
authParams: {
issuer: this.config.issuer,
responseType: 'code',
scopes: ['openid', 'email', 'profile'],
},
});
signIn.renderEl({ el: '#sign-in-container' }, () => {});
}
}To perform the Authorization Code Flow, we set the responseType to code. This returns an access_token and/or id_token through the /token OpenID Connect endpoint.
Note: Additional configuration for the SignIn object is available at OpenID Connect, OAuth 2.0, and Social Auth with Okta.
By default, this end-to-end sample ships with our Angular 1 front-end sample. To run this back-end with a different front-end:
-
Choose the front-end
Framework NPM module Github Angular 1 @okta/samples-js-angular-1 https://github.com/okta/samples-js-angular-1 React @okta/samples-js-react https://github.com/okta/samples-js-react Elm @okta/samples-elm https://github.com/okta/samples-elm -
Install the front-end
# Use the NPM module for the front-end you want to install. I.e. for React: [samples-python-flask]$ npm install @okta/samples-js-react -
Restart the server. You should be up and running with the new front-end!
To complete the Authorization Code Flow, your back-end server performs the following tasks:
- Handle the Authorization Code code exchange callback
- Validate the
id_token - Set
usersession in the app - Log the user out
To render the AngularJS templates, we define the following Flask routes:
| Route | Description |
|---|---|
| authorization-code/login-redirect | renders the login redirect flow |
| authorization-code/login-custom | renders the custom login flow |
| authorization-code/callback | handles the redirect from Okta |
| authorization-code/profile | renders the logged in state, displaying profile information |
| authorization-code/logout | closes the user session |
After successful authentication, an authorization code is returned to the redirectUri:
http://localhost:3000/authorization-code/callback?code={{code}}&state={{state}}
Two cookies are created after authentication: okta-oauth-nonce and okta-auth-state. You must verify the returned state value in the URL matches the state value created.
In this sample, we verify the state here:
cookies = request.cookies
if (('okta-oauth-nonce' in cookies) and ('okta-oauth-state' in cookies)):
nonce = cookies['okta-oauth-nonce']
state = cookies['okta-oauth-state']
else:
return "invalid nonce or state", 401
if (request.args.get('state') != state):
err = "'{}' != '{}'".format(
request.args.get('state'),
state)
return "invalid state: {}".format(err), 401Next, we exchange the returned authorization code for an id_token and/or access_token. You can choose the best token authentication method for your application. In this sample, we use the default token authentication method client_secret_basic:
auth = HTTPBasicAuth(config['oidc']['clientId'],
config['oidc']['clientSecret'])
querystring = {
'grant_type': 'authorization_code',
'code': request.args.get('code'),
'redirect_uri': config['oidc']['redirectUri']
}
url = "{}/v1/token".format(config['oidc']['issuer'])
qs = "grant_type=authorization_code&code={}&redirect_uri={}".format(
urllib.quote_plus(querystring['code']),
urllib.quote_plus(querystring['redirect_uri'])
)
url = "{}/v1/token?{}".format(config['oidc']['issuer'], qs)
headers = {
'User-Agent': None,
'Connection': 'close',
'Accept': 'application/json',
'Content-Type': 'application/x-www-form-urlencoded'
}
r = requests.post(url,
stream=False,
auth=auth,
headers=headers)
return_value = r.json()
if 'id_token' not in return_value:
return "no id_token in response from /token endpoint", 401
id_token = return_value['id_token']A successful response returns an id_token which looks similar to:
eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiIwMHVpZDRCeFh3Nkk2VFY0bTBnMyIsImVtYWlsIjoid2VibWFzd
GVyQGNsb3VkaXR1ZGUubmV0IiwiZW1haWxfdmVyaWZpZWQiOnRydWUsInZlciI6MSwiaXNzIjoiaHR0cD
ovL3JhaW4ub2t0YTEuY29tOjE4MDIiLCJsb2dpbiI6ImFkbWluaXN0cmF0b3IxQGNsb3VkaXR1ZGUu
bmV0IiwiYXVkIjoidUFhdW5vZldrYURKeHVrQ0ZlQngiLCJpYXQiOjE0NDk2MjQwMjYsImV4cCI6MTQ0O
TYyNzYyNiwiYW1yIjpbInB3ZCJdLCJqdGkiOiI0ZUFXSk9DTUIzU1g4WGV3RGZWUiIsImF1dGhfdGltZSI
6MTQ0OTYyNDAyNiwiYXRfaGFzaCI6ImNwcUtmZFFBNWVIODkxRmY1b0pyX1EifQ.Btw6bUbZhRa89
DsBb8KmL9rfhku--_mbNC2pgC8yu8obJnwO12nFBepui9KzbpJhGM91PqJwi_AylE6rp-
ehamfnUAO4JL14PkemF45Pn3u_6KKwxJnxcWxLvMuuisnvIs7NScKpOAab6ayZU0VL8W6XAijQmnYTt
MWQfSuaaR8rYOaWHrffh3OypvDdrQuYacbkT0csxdrayXfBG3UF5-
ZAlhfch1fhFT3yZFdWwzkSDc0BGygfiFyNhCezfyT454wbciSZgrA9ROeHkfPCaX7KCFO8GgQEkGRoQ
ntFBNjluFhNLJIUkEFovEDlfuB4tv_M8BM75celdy3jkpOurg
After receiving the id_token, we validate the token and its claims to prove its integrity.
In this sample, we use a JSON Object Signing and Encryption (JOSE) library to decode and validate the token.
There are a couple things we need to verify:
- Verify the signature
- Verify the iss (issuer), aud (audience), and exp (expiry) time
- Verify the iat (issued at) time
- Verify the nonce
You can learn more about validating tokens in OpenID Connect Resources.
An id_token contains a public key id (kid). To verify the signature, we use the Discovery Document to find the jwks_uri, which will return a list of public keys. It is safe to cache or persist these keys for performance, but Okta rotates them periodically. We strongly recommend dynamically retrieving these keys.
For example:
- If the
kidhas been cached, use it to validate the signature. - If not, make a request to the
jwks_uri. Cache the newjwks, and use the response to validate the signature.
def fetch_jwk_for(id_token=None):
if id_token is None:
raise NameError('id_token is required')
jwks_uri = "{}/v1/keys".format(config['oidc']['issuer'])
unverified_header = jws.get_unverified_header(id_token)
key_id = None
if 'kid' in unverified_header:
key_id = unverified_header['kid']
else:
raise ValueError('The id_token header must contain a "kid"')
if key_id in public_key_cache:
return public_key_cache[key_id]
r = requests.get(jwks_uri)
jwks = r.json()
for key in jwks['keys']:
jwk_id = key['kid']
public_key_cache[jwk_id] = key
if key_id in public_key_cache:
return public_key_cache[key_id]
else:
raise RuntimeError("Unable to fetch public key from jwks_uri")Verify the id_token from the Code Exchange contains our expected claims:
- The
issueris identical to the host where authorization was performed - The
clientIdstored in our configuration matches theaudclaim - If the token expiration time has passed, the token must be revoked
five_minutes_in_seconds = 300
leeway = five_minutes_in_seconds
jwt_kwargs = {
'algorithms': 'RS256',
'options': {
'verify_at_hash': False,
# Used for leeway on the "exp" claim
'leeway': leeway
},
'issuer': config['oidc']['issuer'],
'audience': config['oidc']['clientId']
}
if 'access_token' in return_value:
jwt_kwargs['access_token'] = return_value['access_token']
try:
jwks_with_public_key = fetch_jwk_for(id_token)
claims = jwt.decode(
id_token,
jwks_with_public_key,
**jwt_kwargs)
except (jose.exceptions.JWTClaimsError,
jose.exceptions.JWTError,
jose.exceptions.JWSError,
jose.exceptions.ExpiredSignatureError,
NameError,
ValueError), err:
return str(err), 401The iat value indicates what time the token was "issued at". We verify that this claim is valid by checking that the token was not issued in the future, with some leeway for clock skew.
time_now_with_leeway = datetime.utcnow() + timedelta(seconds=leeway)
acceptable_iat = calendar.timegm((time_now_with_leeway).timetuple())
if 'iat' in claims and claims['iat'] > acceptable_iat:
return "invalid iat claim", 401To mitigate replay attacks, verify that the nonce value in the id_token matches the nonce stored in the cookie okta-oauth-nonce.
if nonce != claims['nonce']:
return "invalid nonce", 401If the id_token passes validation, we can then set the user session in our application.
In a production app, this code would lookup the user from a user store and set the session for that user. However, for simplicity, in this sample we set the session with the claims from the id_token.
session['user'] = {
'email': claims['email'],
'claims': claims
}In Flask, you can clear the the user session by:
session.clear()The Okta session is terminated in our client-side code.
You have now successfully authenticated with Okta! Now what? With a user's id_token, you have basic claims into the user's identity. You can extend the set of claims by modifying the response_type and scopes to retrieve custom information about the user. This includes locale, address, phone_number, groups, and more.
Have a question or see a bug? Email developers@okta.com. For feature requests, feel free to open an issue on this repo. If you find a security vulnerability, please follow our Vulnerability Reporting Process.
Copyright 2017 Okta, Inc. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

