|
| 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 | +} |
0 commit comments