Skip to content

Commit 8c8fbf3

Browse files
Feature/add authx sample (#15)
1 parent 02c0307 commit 8c8fbf3

File tree

16 files changed

+661
-9
lines changed

16 files changed

+661
-9
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ The first time you run a Jupyter notebook, you'll be asked to install the Jupyte
4949
|:-----------------|:----------------------------------------------------------------------------|:-------------------------------------------|
5050
| [General](./samples/general/create.ipynb) | Basic demo of APIM sample setup and policy usage. | All infrastructures |
5151
| [Load Balancing](./samples/load-balancing/create.ipynb) | Priority and weighted load balancing across backends. | apim-aca, afd-apim (with ACA) |
52+
| [AuthX](./samples/authx/create.ipynb) | Authentication and role-based authorization in a mock HR API. | All infrastructures |
5253

5354
### Running a Sample
5455

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,6 @@ requests
44
setuptools
55
pandas
66
matplotlib
7+
pyjwt
78
pytest
89
pytest-cov

samples/_TEMPLATE/create.ipynb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@
1818
"1. [LEARNING / EXPERIMENTATION OBJECTIVE 2]\n",
1919
"1. ...\n",
2020
"\n",
21+
"## Scenario\n",
22+
"\n",
23+
"[IF THE SAMPLE IS DEMONSTRATED THROUGH A USE CASE OR SCENARIO, PLEASE DETAIL IT HERE. OTHERWISE, DELETE THIS SECTION]\n",
24+
"\n",
2125
"## Lab Components\n",
2226
"\n",
2327
"[DESCRIBE IN MORE DETAIL WHAT THIS LAB SETS UP AND HOW THIS BENEFITS THE LEARNER/USER.]\n",

samples/authX/create.ipynb

Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
{
2+
"cells": [
3+
{
4+
"cell_type": "markdown",
5+
"metadata": {},
6+
"source": [
7+
"# Samples: AuthX - Authentication & Authorization\n",
8+
"\n",
9+
"[BRIEF SAMPLE DESCRIPTION]\n",
10+
"\n",
11+
"⚙️ **Supported infrastructures**: All infrastructures\n",
12+
"\n",
13+
"⌚ **Expected *Run All* runtime (excl. infrastructure prerequisite): ~[NOTEBOOK RUNTIME] minute**\n",
14+
"\n",
15+
"## Objectives\n",
16+
"\n",
17+
"1. Understand how API Management supports OAuth 2.0 authentication (authN) with JSON Web Tokens (JWT).\n",
18+
"1. Learn how authorization (authZ) can be accomplished based on JWT claims.\n",
19+
"1. Configure authN and authZ at various levels in the API Management hierarchy.\n",
20+
"1. Use external secrets in policies.\n",
21+
"\n",
22+
"## Scenario\n",
23+
"\n",
24+
"This sample combines _authentication (authN)_ and _authorization (authZ)_ into _authX_. This scenario focuses on a Human Resources API that requires privileged role-based access to GET and to POST data. This is simplistic but shows the combination of authN and authZ.\n",
25+
"\n",
26+
"There are two personas at play:\n",
27+
"\n",
28+
"- `HR Administrator` - holds broad rights to the API\n",
29+
"- `HR Associate` - has read-only permissions\n",
30+
"\n",
31+
"Both personas are part of an HR_Members group and may access the HR API Management Product. Subsequent access to the APIs and their operations must be granular.\n",
32+
"\n",
33+
"### Notes\n",
34+
"\n",
35+
"Many organizations require 100% authentication for their APIs. While that is prudent and typically done at the global _All APIs_ level, we refrain from doing so here as to not impact other samples. Instead, we focus on authentication at the API Management API and API operation levels.\n",
36+
"\n",
37+
"## Lab Components\n",
38+
"\n",
39+
"While OAuth 2.0 includes an identity provider (IDP), for sake of the sample, we can remove the complexity of including real identities. It is sufficient to use mock JWTs that we can \"authenticate\" by way of a signing key. This is a valid, albeit not the default method for authentication. \n",
40+
"\n",
41+
"We do not need real APIs and can rely on mock returns.\n",
42+
"\n",
43+
"Furthermore, secrets would ideally be kept in a secret store such as Azure Key Vault and be accessed via API Management's managed identity. Adding a Key Vault to our architecture is a stretch goal that provides value but is not immediately necessary to showcase the authX sample.\n",
44+
"\n",
45+
"JSON Web Tokens are defined in [RFC 7519](https://www.rfc-editor.org/rfc/rfc7519). Two websites to use with JWTs are [Okta's](https://jwt.io/) and [Microsoft's](https://jwt.ms/). Okta's may be preferential due to its features.\n",
46+
"\n",
47+
"## Configuration\n",
48+
"\n",
49+
"1. Decide which of the [Infrastructure Architectures](../../README.md#infrastructure-architectures) you wish to use.\n",
50+
" 1. If the infrastructure _does not_ yet exist, navigate to the desired [infrastructure](../../infrastructure/) folder and follow its README.md.\n",
51+
" 1. If the infrastructure _does_ exist, adjust the `user-defined parameters` in the _Initialize notebook variables_ below. Please ensure that all parameters match your infrastructure."
52+
]
53+
},
54+
{
55+
"cell_type": "markdown",
56+
"metadata": {},
57+
"source": [
58+
"### Initialize notebook variables\n",
59+
"\n",
60+
"Configures everything that's needed for deployment. \n",
61+
"\n",
62+
"[ADD ANY SPECIAL INSTRUCTIONS]\n",
63+
"\n",
64+
"**Modify entries under _1) User-defined parameters_ and _3) Define the APIs and their operations and policies_**."
65+
]
66+
},
67+
{
68+
"cell_type": "code",
69+
"execution_count": null,
70+
"metadata": {},
71+
"outputs": [],
72+
"source": [
73+
"import utils\n",
74+
"import time\n",
75+
"from apimtypes import *\n",
76+
"\n",
77+
"# 1) User-defined parameters (change these as needed)\n",
78+
"rg_location = 'eastus2'\n",
79+
"index = 1\n",
80+
"deployment = INFRASTRUCTURE.SIMPLE_APIM\n",
81+
"tags = ['authX', 'jwt', 'hr'] # ENTER DESCRIPTIVE TAG(S)\n",
82+
"api_prefix = 'authX-' # OPTIONAL: ENTER A PREFIX FOR THE APIS TO REDUCE COLLISION POTENTIAL WITH OTHER SAMPLES\n",
83+
"\n",
84+
"# 2) Service-defined parameters (please do not change these)\n",
85+
"rg_name = utils.get_infra_rg_name(deployment, index)\n",
86+
"supported_infrastructures = [INFRASTRUCTURE.SIMPLE_APIM, INFRASTRUCTURE.AFD_APIM_PE, INFRASTRUCTURE.APIM_ACA] # ENTER SUPPORTED INFRASTRUCTURES HERE, e.g., [INFRASTRUCTURE.AFD_APIM_PE, INFRASTRUCTURE.AFD_APIM_FE]\n",
87+
"utils.validate_infrastructure(deployment, supported_infrastructures)\n",
88+
"\n",
89+
"# Set up the signing key for the JWT policy\n",
90+
"jwt_key_name = f'JwtSigningKey{int(time.time())}'\n",
91+
"jwt_key_value, jwt_key_value_bytes_b64 = utils.generate_signing_key()\n",
92+
"utils.print_val('JWT key value', jwt_key_value) # this value is used to create the signed JWT token for requests to APIM\n",
93+
"utils.print_val('JWT key value (base64)', jwt_key_value_bytes_b64) # this value is used in the APIM validate-jwt policy's issuer-signing-key attribute \n",
94+
"\n",
95+
"# 3) Define the APIs and their operations and policies\n",
96+
"\n",
97+
"# Policies\n",
98+
"# Named values must be set up a bit differently as they need to have two surrounding curly braces\n",
99+
"hr_all_operations_xml = utils.read_policy_xml('./hr_all_operations.xml').format(\n",
100+
" jwt_signing_key = '{{' + jwt_key_name + '}}', \n",
101+
" hr_member_role_id = '{{HRMemberRoleId}}'\n",
102+
")\n",
103+
"hr_get_xml = utils.read_policy_xml('./hr_get.xml').format(\n",
104+
" hr_administrator_role_id = '{{HRAdministratorRoleId}}',\n",
105+
" hr_associate_role_id = '{{HRAssociateRoleId}}'\n",
106+
")\n",
107+
"hr_post_xml = utils.read_policy_xml('./hr_post.xml').format(\n",
108+
" hr_administrator_role_id = '{{HRAdministratorRoleId}}'\n",
109+
")\n",
110+
"\n",
111+
"# Employees (HR)\n",
112+
"hremployees_get = GET_APIOperation('Gets the employees', hr_get_xml)\n",
113+
"hremployees_post = POST_APIOperation('Creates a new employee', hr_post_xml)\n",
114+
"hremployees = API('Employees', 'Employees', '/employees', 'This is a Human Resources API to obtain employee information', hr_all_operations_xml, operations = [hremployees_get, hremployees_post], tags = tags)\n",
115+
"\n",
116+
"# APIs Array\n",
117+
"apis: List[API] = [hremployees]\n",
118+
"\n",
119+
"# 4) Set up the named values\n",
120+
"nvs: List[NamedValue] = [\n",
121+
" NamedValue(jwt_key_name, jwt_key_value_bytes_b64, True),\n",
122+
" NamedValue('HRMemberRoleId', HR_MEMBER_ROLE_ID),\n",
123+
" NamedValue('HRAssociateRoleId', HR_ASSOCIATE_ROLE_ID),\n",
124+
" NamedValue('HRAdministratorRoleId', HR_ADMINISTRATOR_ROLE_ID)\n",
125+
"]\n",
126+
"\n",
127+
"utils.print_ok('Notebook initialized')"
128+
]
129+
},
130+
{
131+
"cell_type": "markdown",
132+
"metadata": {},
133+
"source": [
134+
"### Create deployment using Bicep\n",
135+
"\n",
136+
"Creates the bicep deployment into the previously-specified resource group. A bicep parameters file will be created prior to execution."
137+
]
138+
},
139+
{
140+
"cell_type": "code",
141+
"execution_count": null,
142+
"metadata": {},
143+
"outputs": [],
144+
"source": [
145+
"import utils\n",
146+
"\n",
147+
"# 1) Define the Bicep parameters with serialized APIs\n",
148+
"bicep_parameters = {\n",
149+
" 'apis': {'value': [api.to_dict() for api in apis]},\n",
150+
" 'namedValues': {'value': [nv.to_dict() for nv in nvs]}\n",
151+
"}\n",
152+
"\n",
153+
"# 2) Infrastructure must be in place before samples can be layered on top\n",
154+
"if not utils.does_resource_group_exist(rg_name):\n",
155+
" utils.print_error(f'The specified infrastructure resource group and its resources must exist first. Please check that the user-defined parameters above are correctly referencing an existing infrastructure. If it does not yet exist, run the desired infrastructure in the /infra/ folder first.')\n",
156+
" raise SystemExit(1)\n",
157+
"\n",
158+
"# 3) Run the deployment\n",
159+
"output = utils.create_bicep_deployment_group(rg_name, rg_location, deployment, bicep_parameters)\n",
160+
"\n",
161+
"# 4) Print a deployment summary, if successful; otherwise, exit with an error\n",
162+
"if not output.success:\n",
163+
" raise SystemExit('Deployment failed')\n",
164+
"\n",
165+
"if output.success and output.json_data:\n",
166+
" apim_gateway_url = output.get('apimResourceGatewayURL', 'APIM API Gateway URL')\n",
167+
"\n",
168+
"utils.print_ok('Deployment completed')"
169+
]
170+
},
171+
{
172+
"cell_type": "markdown",
173+
"metadata": {},
174+
"source": [
175+
"### Verify API Request Success\n",
176+
"\n",
177+
"Assert that the deployment was successful by making simple calls to APIM. \n",
178+
"\n",
179+
"❗️ If the infrastructure shields APIM and requires a different ingress (e.g. Azure Front Door), the request to the APIM gateway URl will fail by design. Obtain the Front Door endpoint hostname and try that instead."
180+
]
181+
},
182+
{
183+
"cell_type": "code",
184+
"execution_count": null,
185+
"metadata": {},
186+
"outputs": [],
187+
"source": [
188+
"import utils\n",
189+
"from apimrequests import ApimRequests\n",
190+
"from apimjwt import JwtPayload, SymmetricJwtToken\n",
191+
"from apimtypes import HR_MEMBER_ROLE_ID, HR_ADMINISTRATOR_ROLE_ID, HR_ASSOCIATE_ROLE_ID\n",
192+
"\n",
193+
"# 1) HR Administrator\n",
194+
"# Create a JSON Web Token with a payload and sign it with the symmetric key from above.\n",
195+
"jwt_payload_hr_admin = JwtPayload(subject = 'user123', name = 'Angie Administrator', roles = [HR_MEMBER_ROLE_ID, HR_ADMINISTRATOR_ROLE_ID])\n",
196+
"encoded_jwt_token_hr_admin = SymmetricJwtToken(jwt_key_value, jwt_payload_hr_admin).encode()\n",
197+
"print(f'JWT token HR Admin: {encoded_jwt_token_hr_admin}') # this value is used to call the APIs via APIM\n",
198+
"\n",
199+
"# Set up an APIM requests object with the JWT token\n",
200+
"reqsApimAdmin = ApimRequests(apim_gateway_url)\n",
201+
"reqsApimAdmin.headers['Authorization'] = f'Bearer {encoded_jwt_token_hr_admin}'\n",
202+
"\n",
203+
"# Call APIM\n",
204+
"reqsApimAdmin.singleGet('/employees', msg = 'Calling GET Employees API via API Management Gateway URL. Expect 200.')\n",
205+
"reqsApimAdmin.singlePost('/employees', msg = 'Calling POST Employees API via API Management Gateway URL. Expect 200.')\n",
206+
"\n",
207+
"# 2) HR Associate\n",
208+
"# Create a JSON Web Token with a payload and sign it with the symmetric key from above.\n",
209+
"jwt_payload_hr_associate = JwtPayload(subject = 'user789', name = 'Aaron Associate', roles = [HR_MEMBER_ROLE_ID, HR_ASSOCIATE_ROLE_ID])\n",
210+
"encoded_jwt_token_hr_associate = SymmetricJwtToken(jwt_key_value, jwt_payload_hr_associate).encode()\n",
211+
"print(f'\\n\\nJWT token HR Associate: {encoded_jwt_token_hr_associate}') # this value is used to call the APIs via APIM\n",
212+
"\n",
213+
"# Set up an APIM requests object with the JWT token\n",
214+
"reqsApimAssociate = ApimRequests(apim_gateway_url)\n",
215+
"reqsApimAssociate.headers['Authorization'] = f'Bearer {encoded_jwt_token_hr_associate}'\n",
216+
"\n",
217+
"# Call APIM\n",
218+
"reqsApimAssociate.singleGet('/employees', msg = 'Calling GET Employees API via API Management Gateway URL. Expect 200.')\n",
219+
"reqsApimAssociate.singlePost('/employees', msg = 'Calling POST Employees API via API Management Gateway URL. Expect 403.')\n",
220+
"\n",
221+
"utils.print_ok('All done!')"
222+
]
223+
}
224+
],
225+
"metadata": {
226+
"kernelspec": {
227+
"display_name": ".venv",
228+
"language": "python",
229+
"name": "python3"
230+
},
231+
"language_info": {
232+
"codemirror_mode": {
233+
"name": "ipython",
234+
"version": 3
235+
},
236+
"file_extension": ".py",
237+
"mimetype": "text/x-python",
238+
"name": "python",
239+
"nbconvert_exporter": "python",
240+
"pygments_lexer": "ipython3",
241+
"version": "3.12.10"
242+
}
243+
},
244+
"nbformat": 4,
245+
"nbformat_minor": 2
246+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<!--
2+
This policy authenticates the caller based on their JSON Web Token. It is not calling an Identity Provider as this is a mock policy.
3+
-->
4+
<policies>
5+
<inbound>
6+
<base />
7+
<validate-jwt header-name="Authorization" failed-validation-httpcode="401" failed-validation-error-message="Unauthorized" output-token-variable-name="jwt">
8+
<issuer-signing-keys>
9+
<key>{jwt_signing_key}</key>
10+
</issuer-signing-keys>
11+
<required-claims>
12+
<claim name="roles" match="all">
13+
<value>{hr_member_role_id}</value>
14+
</claim>
15+
</required-claims>
16+
</validate-jwt>
17+
</inbound>
18+
<backend>
19+
<base />
20+
</backend>
21+
<outbound>
22+
<base />
23+
</outbound>
24+
<on-error>
25+
<base />
26+
</on-error>
27+
</policies>

samples/authX/hr_get.xml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<!--
2+
This policy authenticates gets mock information about employees. The payload is not relevant for this sample. We are only interested in the authX aspects.
3+
-->
4+
<policies>
5+
<inbound>
6+
<base />
7+
<choose>
8+
<!-- HR Administrators and HR Associates can both obtain employee information -->
9+
<when condition="@(((Jwt)context.Variables[&quot;jwt&quot;]).Claims[&quot;roles&quot;].Contains(&quot;{hr_administrator_role_id}&quot;) || ((Jwt)context.Variables[&quot;jwt&quot;]).Claims[&quot;roles&quot;].Contains(&quot;{hr_associate_role_id}&quot;))">
10+
<return-response>
11+
<set-status code="200" reason="OK" />
12+
<set-body>Returning a mock employee</set-body>
13+
</return-response>
14+
</when>
15+
<otherwise>
16+
<return-response>
17+
<set-status code="403" reason="Forbidden" />
18+
</return-response>
19+
</otherwise>
20+
</choose>
21+
</inbound>
22+
<backend>
23+
<base />
24+
</backend>
25+
<outbound>
26+
<base />
27+
</outbound>
28+
<on-error>
29+
<base />
30+
</on-error>
31+
</policies>

samples/authX/hr_post.xml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<!--
2+
This policy mocks the creation of a new employee. The payload is not relevant for this sample. We are only interested in the authX aspects.
3+
-->
4+
<policies>
5+
<inbound>
6+
<base />
7+
<choose>
8+
<!-- Only HR Administrators can create employees -->
9+
<when condition="@(((Jwt)context.Variables[&quot;jwt&quot;]).Claims[&quot;roles&quot;].Contains(&quot;{hr_administrator_role_id}&quot;))">
10+
<return-response>
11+
<set-status code="200" reason="OK" />
12+
<set-body>A mock employee has been created.</set-body>
13+
</return-response>
14+
</when>
15+
<otherwise>
16+
<return-response>
17+
<set-status code="403" reason="Forbidden" />
18+
</return-response>
19+
</otherwise>
20+
</choose>
21+
</inbound>
22+
<backend>
23+
<base />
24+
</backend>
25+
<outbound>
26+
<base />
27+
</outbound>
28+
<on-error>
29+
<base />
30+
</on-error>
31+
</policies>

0 commit comments

Comments
 (0)