Skip to content

Commit 8ba49aa

Browse files
styk-tvvimalloc
authored andcommitted
full oidc example with autoconfiguration (#222)
1 parent f300015 commit 8ba49aa

File tree

1 file changed

+137
-0
lines changed

1 file changed

+137
-0
lines changed

examples/oidc.py

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
from flask import Flask, jsonify
2+
from flask_restful import Api
3+
import requests
4+
import json
5+
from jwt.algorithms import RSAAlgorithm
6+
from functools import wraps
7+
from flask_jwt_extended import (
8+
JWTManager, verify_jwt_in_request, get_raw_jwt, current_user
9+
)
10+
import config
11+
12+
13+
# Setup Flask Server
14+
app = Flask(__name__)
15+
app.config.from_object(config.Config)
16+
api = Api(app)
17+
18+
# ==== SETUP
19+
# Set OIDC entries for auto-discovery
20+
# Naming of 4 key input variables borrowed from Kubernetes (https://kubernetes.io/docs/reference/access-authn-authz/authentication/)
21+
# This was tested with Red Hat's https://www.keycloak.org/ but should work with any OIDC provider like Auth0, Octa, Google, Microsoft etc.
22+
23+
# Issuer + Realm
24+
OIDC_ISSUER_URL = 'https://my-identity-server.example/auth/realms/master'
25+
26+
# Client ID (Audience)
27+
OIDC_CLIENT_ID = 'example.my-identity-server'
28+
29+
# Token variable holding unique username
30+
OIDC_USERNAME_CLAIM = 'email'
31+
32+
# Token list variable holding groups that user belongs to (for role-based-access-control)
33+
# idea here is to have few but could be hundreds of groups, based on which groups user belongs to, grants them access to various endpoints
34+
# in identity server this is usually mapped directly to ldap, so ldap group membership defines which endpoints user can access
35+
# https://www.keycloak.org/docs/latest/server_admin/index.html#_ldap_mappers, but remember groups don't have to come from ldap
36+
# group mapper was setup for flat group structure not to include any prefixes so if you have to do that, please update code in group_required method
37+
OIDC_GROUPS_CLAIM = 'groups'
38+
39+
# ==== END OF SETUP
40+
41+
42+
# Helper Methods
43+
def urljoin(*args):
44+
"""
45+
Joins given arguments into an url. Trailing but not leading slashes are
46+
stripped for each argument.
47+
"""
48+
49+
return "/".join(map(lambda x: str(x).rstrip('/'), args))
50+
51+
52+
def token_required(fn):
53+
@wraps(fn)
54+
def wrapper(*args, **kwargs):
55+
verify_jwt_in_request()
56+
return fn(*args, **kwargs)
57+
return wrapper
58+
59+
60+
def group_required(group=''):
61+
def decorator(fn):
62+
@wraps(fn)
63+
def wrapper(*args, **kwargs):
64+
# standard flask_jwt_extended token verifications
65+
verify_jwt_in_request()
66+
67+
# custom group membership verification
68+
groups = get_raw_jwt()[OIDC_GROUPS_CLAIM]
69+
if group not in groups:
70+
return jsonify({'result': "user not in group required to access this endpoint"}), 401
71+
return fn(*args, **kwargs)
72+
return wrapper
73+
return decorator
74+
75+
76+
# Setup Token Verification
77+
# force use of RS265
78+
app.config['JWT_ALGORITHM'] = 'RS256'
79+
80+
# retrieve master openid-configuration endpoint for issuer realm
81+
oidc_config = requests.get(urljoin(OIDC_ISSUER_URL, '.well-known/openid-configuration'), verify=False).json()
82+
83+
# retrieve data from jwks_uri endpoint
84+
oidc_jwks_uri = requests.get(oidc_config['jwks_uri'], verify=False).json()
85+
86+
# retrieve first jwk entry from jwks_uri endpoint and use it to construct the RSA public key
87+
app.config['JWT_PUBLIC_KEY'] = RSAAlgorithm.from_jwk(json.dumps(oidc_jwks_uri['keys'][0]))
88+
89+
# audience is oidc client id (can be array starting https://github.com/vimalloc/flask-jwt-extended/issues/219)
90+
app.config['JWT_DECODE_AUDIENCE'] = OIDC_CLIENT_ID
91+
92+
# name of token entry that will become distinct flask identity username
93+
app.config['JWT_IDENTITY_CLAIM'] = OIDC_USERNAME_CLAIM
94+
jwt = JWTManager(app)
95+
96+
97+
# TEST ENDPOINTS
98+
@app.route('/anonymous', methods=['GET'])
99+
def get_anonymous():
100+
return jsonify({'result': "anonymous ok"}), 200
101+
102+
103+
@app.route('/token-protected', methods=['GET'])
104+
@token_required
105+
def get_protected_by_token():
106+
return jsonify({'result': "protected by token ok"}), 200
107+
108+
109+
@app.route('/group-protected', methods=['GET'])
110+
@group_required('api-access') # currently one, could be one of or multiple required depending on your needs
111+
def get_protected_by_group():
112+
return jsonify({'result': "protected by token AND group membership ok"},
113+
{'user': current_user.username}
114+
), 200
115+
116+
117+
# Identity User Class
118+
class User:
119+
username = None
120+
121+
def __init__(self):
122+
pass
123+
124+
125+
# User Class to get you started
126+
# Identity holds whatever variable in token you point at JWT_IDENTITY_CLAIM
127+
# good place to construct identity from token and other places, it is then available in method through current_user.<property>
128+
@jwt.user_loader_callback_loader
129+
def user_loader_callback(identity):
130+
u = User()
131+
u.username = identity
132+
return u
133+
134+
135+
app.run(host='0.0.0.0')
136+
137+

0 commit comments

Comments
 (0)