Skip to content

Commit 94a3e0f

Browse files
Feature/credential manager (#64)
1 parent 07d4fab commit 94a3e0f

File tree

8 files changed

+557
-8
lines changed

8 files changed

+557
-8
lines changed

README.md

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -139,13 +139,14 @@ For detailed troubleshooting of setup issues, see [Import Troubleshooting Guide]
139139

140140
### 📁 List of Samples
141141

142-
| Sample Name | Description | Supported Infrastructure(s) |
143-
|:----------------------------------------------------------------|:--------------------------------------------------------------------------------------------------------------------|:------------------------------|
144-
| [AuthX](./samples/authX/README.md) | Authentication and role-based authorization in a mock HR API. | All infrastructures |
145-
| [AuthX Pro](./samples/authX-pro/README.md) | Authentication and role-based authorization in a mock product with multiple APIs and policy fragments. | All infrastructures |
146-
| [General](./samples/general/README.md) | Basic demo of APIM sample setup and policy usage. | All infrastructures |
147-
| [Load Balancing](./samples/load-balancing/README.md) | Priority and weighted load balancing across backends. | apim-aca, afd-apim (with ACA) |
148-
| [Secure Blob Access](./samples/secure-blob-access/README.md) | Secure blob access via the [valet key pattern](https://learn.microsoft.com/azure/architecture/patterns/valet-key). | All infrastructures |
142+
| Sample Name | Description | Supported Infrastructure(s) |
143+
|:-------------------------------------------------------------------------|:--------------------------------------------------------------------------------------------------------------------|:------------------------------|
144+
| [AuthX](./samples/authX/README.md) | Authentication and role-based authorization in a mock HR API. | All infrastructures |
145+
| [AuthX Pro](./samples/authX-pro/README.md) | Authentication and role-based authorization in a mock product with multiple APIs and policy fragments. | All infrastructures |
146+
| [General](./samples/general/README.md) | Basic demo of APIM sample setup and policy usage. | All infrastructures |
147+
| [Load Balancing](./samples/load-balancing/README.md) | Priority and weighted load balancing across backends. | apim-aca, afd-apim (with ACA) |
148+
| [Secure Blob Access](./samples/secure-blob-access/README.md) | Secure blob access via the [valet key pattern](https://learn.microsoft.com/azure/architecture/patterns/valet-key). | All infrastructures |
149+
| [Credential Manager (with Spotify)](./samples/oauth-3rd-party/README.md) | Authenticate with APIM which then uses its Credential Manager with Spotify's REST API. | All infrastructures |
149150

150151
### ▶️ Running a Sample
151152

samples/oauth-3rd-party/README.md

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# Samples: OAuth 2.0 with 3rd Party
2+
3+
Sets up a 3rd party integration via [Azure API Management Credential Manager](https://learn.microsoft.com/azure/api-management/credentials-overview).
4+
5+
***This sample has prerequisites! Please follow the instructions below.***
6+
7+
⚙️ **Supported infrastructures**: All infrastructures
8+
9+
👟 **Expected *Run All* runtime (excl. infrastructure prerequisite): ~2-3 minutes**
10+
11+
## 🎯 Objectives
12+
13+
1. Distinguish between authentication to APIM via JSON Web Tokens and to the 3rd party using Credential Manager.
14+
1. Understand how API Management supports OAuth 2.0 authentication (authN) with JSON Web Tokens (JWT).
15+
1. Learn how authorization (authZ) can be accomplished based on JWT claims.
16+
1. Configure authN and authZ at the API level (simpler than _AuthX-Pro_)
17+
1. Use external secrets in policies.
18+
1. Experience how API Management policy fragments simplify shared logic.
19+
20+
## 📝 Scenario
21+
22+
We chose Spotify as it provides an extensive REST API and has relatively generous limits on free API access. This makes for a relatively straight-forward experience for this sample.
23+
Specifically, this sample uses Spotify's REST API to obtain information about its deep music and artist catalog. API Management is registered as an application in Spotify's applications with its own client ID and client secret for a given scope. This application is then set up as a generic OAuth 2.0 integration in Credential Manager.
24+
Furthermore, we build on the knowledge gained from the _AuthX_ and _AuthX-Pro_ samples to authentication callers and authorize their use of the Spotify integration.
25+
26+
We use only one persona in this sample:
27+
28+
- `Marketing Member` - holds read rights.
29+
30+
The API hierarchy is as follows:
31+
32+
1. All APIs / global
33+
This is a great place to do authentication, but we refrain from doing it in the sample as to not affect other samples.
34+
1. Marketing Member
35+
36+
## Prerequisites
37+
38+
This sample requires a little bit of manual pre-work in order to create a high-fidelity setup:
39+
40+
1. A Spotify Account
41+
1. A Spotify Application
42+
43+
### A Spotify Account
44+
45+
1. You can use your existing Spotify account or sign up for a new one [here](https://www.spotify.com/us/signup). Please ensure you adhere to Spotify's terms & conditions of use.
46+
47+
### A Spotify Application
48+
49+
In order for API Management to gain access to Spotify's API, we need to create an application that represents API Management.
50+
51+
1. Open or log into the [Spotify Developer Dashboard](https://developer.spotify.com/dashboard).
52+
1. Review and accept the _Spotify Developer Terms of Service_, if required.
53+
1. Proceed with verifying your email address, if required.
54+
1. If the Dashboard does not open immediately, select it from the menu after clicking on your profile name (top-right corner).
55+
1. [Create the app](https://developer.spotify.com/dashboard/create):
56+
- **App Name**: _APIM_
57+
- **App Description**: _API Management_
58+
- **Redirect URIs**: https://localhost:8080/callback
59+
We will update this placeholder once we have the APIM URL.
60+
- **Which API/SDKs are you planning to use?** _Web API_
61+
1. Once the app has been created, **note the _Client ID_ and _Client secret_**. We will need them for the Credential Manager setup.
62+
1. Leave the Dashboard page open in your browser, as we will need to replaec the Redirect URI shortly.
63+
1. Proceed to the [create](./create.ipynb) Jupyter notebook and follow directions there.
64+
65+
## Acknowledgement
66+
67+
We thank [Spotify](https://www.spotify.com) for access to their API. Keep building great products!
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<!--
2+
This policy retrieves artist information from Spotify.
3+
-->
4+
<policies>
5+
<inbound>
6+
<base />
7+
<rewrite-uri template="/artists/{id}" copy-unmatched-params="false" />
8+
</inbound>
9+
<backend>
10+
<base />
11+
</backend>
12+
<outbound>
13+
<base />
14+
</outbound>
15+
<on-error>
16+
<base />
17+
</on-error>
18+
</policies>
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+
"### 🛠️ 1. Initialize notebook variables\n",
8+
"\n",
9+
"❗️ **Run cells 1 & 2 MANUALLY (not via _Run All_)!**\n",
10+
"\n",
11+
"Configures everything that's needed for deployment. \n",
12+
"\n",
13+
"👉 **Modify entries under _1) User-defined parameters_ and _3) Define the APIs and their operations and policies_**."
14+
]
15+
},
16+
{
17+
"cell_type": "code",
18+
"execution_count": null,
19+
"metadata": {},
20+
"outputs": [],
21+
"source": [
22+
"import utils\n",
23+
"import time\n",
24+
"from apimtypes import *\n",
25+
"\n",
26+
"# 1) User-defined parameters (change these as needed)\n",
27+
"rg_location = 'eastus2'\n",
28+
"index = 1\n",
29+
"deployment = INFRASTRUCTURE.SIMPLE_APIM\n",
30+
"tags = ['oauth-3rd-party', 'jwt', 'credential-manager', 'policy-fragment'] # ENTER DESCRIPTIVE TAG(S)\n",
31+
"api_prefix = 'oauth-3rd-party-' # OPTIONAL: ENTER A PREFIX FOR THE APIS TO REDUCE COLLISION POTENTIAL WITH OTHER SAMPLES\n",
32+
"# OAuth\n",
33+
"client_id = 'your-spotify-client-id' # ENTER THE OAUTH CLIENT ID FOR THE BACKEND API\n",
34+
"client_secret = 'your-spotify-client-secret' # ENTER THE OAUTH CLIENT SECRET FOR THE BACKEND API\n",
35+
"\n",
36+
"# 2) Service-defined parameters (please do not change these)\n",
37+
"rg_name = utils.get_infra_rg_name(deployment, index)\n",
38+
"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",
39+
"utils.validate_infrastructure(deployment, supported_infrastructures)\n",
40+
"sample_folder = \"oauth-3rd-party\"\n",
41+
"\n",
42+
"# Set up the signing key for the JWT policy\n",
43+
"jwt_key_name = f'JwtSigningKey{int(time.time())}'\n",
44+
"jwt_key_value, jwt_key_value_bytes_b64 = utils.generate_signing_key()\n",
45+
"utils.print_val('JWT key value', jwt_key_value) # this value is used to create the signed JWT token for requests to APIM\n",
46+
"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",
47+
"\n",
48+
"\n",
49+
"# 4) Set up the named values\n",
50+
"nvs: List[NamedValue] = [\n",
51+
" NamedValue(jwt_key_name, jwt_key_value_bytes_b64, True),\n",
52+
" NamedValue('MarketingMemberRoleId', Role.MARKETING_MEMBER)\n",
53+
"]\n",
54+
"\n",
55+
"# 5) Define the APIs and their operations and policies\n",
56+
"\n",
57+
"# Policies\n",
58+
"pol_artist_get_xml = utils.read_policy_xml('artist_get.xml', sample_name = sample_folder)\n",
59+
"\n",
60+
"# Read the policy XML without modifications - it already uses correct APIM named value format\n",
61+
"pol_spotify_api_xml = utils.read_and_modify_policy_xml('spotify_api.xml', {\n",
62+
" 'jwt_signing_key': '{{' + jwt_key_name + '}}', \n",
63+
" 'marketing_member_role_id': '{{MarketingMemberRoleId}}'\n",
64+
"}, sample_folder) \n",
65+
"\n",
66+
"# Define template parameters for the artists\n",
67+
"blob_template_parameters = [\n",
68+
" {\n",
69+
" \"name\": \"id\",\n",
70+
" \"description\": \"The Spotify ID of the artist\",\n",
71+
" \"type\": \"string\",\n",
72+
" \"required\": True\n",
73+
" }\n",
74+
"]\n",
75+
"\n",
76+
"# Spotify\n",
77+
"spotify_artist_get = GET_APIOperation2('artists-get', 'Artists', '/artists/{id}', 'Gets the artist by their ID', pol_artist_get_xml, templateParameters = blob_template_parameters)\n",
78+
"\n",
79+
"# APIs Array\n",
80+
"apis: List[API] = [\n",
81+
" API(f'{api_prefix}spotify', 'Spotify', f'/{api_prefix}spotify', 'This is the API for interactions with the Spotify REST API', policyXml = pol_spotify_api_xml, operations = [spotify_artist_get], tags = tags),\n",
82+
"]\n",
83+
"\n",
84+
"utils.print_ok('Notebook initialized')"
85+
]
86+
},
87+
{
88+
"cell_type": "markdown",
89+
"metadata": {},
90+
"source": [
91+
"### 🚀 2. Create deployment using Bicep\n",
92+
"\n",
93+
"Creates the bicep deployment into the previously-specified resource group. A bicep parameters file will be created prior to execution."
94+
]
95+
},
96+
{
97+
"cell_type": "code",
98+
"execution_count": null,
99+
"metadata": {},
100+
"outputs": [],
101+
"source": [
102+
"import utils\n",
103+
"\n",
104+
"# 1) Define the Bicep parameters with serialized APIs\n",
105+
"bicep_parameters = {\n",
106+
" 'apis': {'value': [api.to_dict() for api in apis]},\n",
107+
" 'namedValues': {'value': [nv.to_dict() for nv in nvs]},\n",
108+
" 'clientId': {'value': client_id},\n",
109+
" 'clientSecret': {'value': client_secret}\n",
110+
"}\n",
111+
"\n",
112+
"# 2) Infrastructure must be in place before samples can be layered on top\n",
113+
"if not utils.does_resource_group_exist(rg_name):\n",
114+
" 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",
115+
" raise SystemExit(1)\n",
116+
"\n",
117+
"# 3) Run the deployment using the utility function that handles working directory management\n",
118+
"output = utils.create_bicep_deployment_group_for_sample(sample_folder, rg_name, rg_location, bicep_parameters)\n",
119+
"\n",
120+
"# 4) Print a deployment summary, if successful; otherwise, exit with an error\n",
121+
"if not output.success:\n",
122+
" raise SystemExit('Deployment failed')\n",
123+
"\n",
124+
"if output.success and output.json_data:\n",
125+
" apim_gateway_url = output.get('apimResourceGatewayURL', 'APIM API Gateway URL')\n",
126+
" apim_service_name = output.get('apimServiceName', 'APIM Service Name')\n",
127+
"\n",
128+
" # TODO: This should be retrieved from an output; however, the format is static.\n",
129+
" apim_oauth_redirect_url = f'https://authorization-manager.consent.azure-apim.net/redirect/apim/{apim_service_name}'\n",
130+
" utils.print_val('APIM OAuth Redirect URL', apim_oauth_redirect_url)\n",
131+
"\n",
132+
"utils.print_ok('Deployment completed')"
133+
]
134+
},
135+
{
136+
"cell_type": "markdown",
137+
"metadata": {},
138+
"source": [
139+
"### 🗒️ 3. Authenticate API Management with Spotify\n",
140+
"\n",
141+
"❗️ **The following steps are all manual and cannot presently be automated.**\n",
142+
"\n",
143+
"We have previously created the _APIM_ application in Spotify and have also set Spotify up in Credential Manager via the just-completed bicep. \n",
144+
"\n",
145+
"#### 3.1 Set Redirect URL in Spotify\n",
146+
"\n",
147+
"Now that the API Management instance has been created, we need to update the redirect URI for the _APIM_ application in Spotify.\n",
148+
"\n",
149+
"1. Open the [Spotify Developer Dashboard](https://developer.spotify.com/dashboard), then click on the _APIM_ application.\n",
150+
"1. Press _Edit_ and remove the temporary _localhost_ Redirect URI.\n",
151+
"1. Add the `APIM OAuth Redirect URL` (see output above), then press 'Save`. \n",
152+
"\n",
153+
"#### 3.2 Log API Management into Spotify\n",
154+
"\n",
155+
"We now need to log the _APIM_ application into Spotify via OAuth 2.0.\n",
156+
"\n",
157+
"1. Open the [Azure Portal](https://portal.azure.com) and navigate to your API Management instance.\n",
158+
"1. Expand the _APIs_ blade and click on _Credential manager_. You should see the `spotify` credential provider name. Click on it.\n",
159+
"1. Press _Connections_. You should see `spotify-auth` with an `Error` status (\"This connection is not authenticated.\").\n",
160+
"1. Click on the ellipsis (...) on the right and select _Login_. This should open a dialog with Spotify, asking you to agree for Spotify and APIM to connect. Press _Agree_.\n",
161+
"1. Back in the Azure Portal, press _Refresh_ to see the `Connected` status.\n"
162+
]
163+
},
164+
{
165+
"cell_type": "markdown",
166+
"metadata": {},
167+
"source": [
168+
"### ✅ 4. Verify API Request Success\n",
169+
"\n",
170+
"Assert that the deployment was successful by making simple calls to APIM. \n",
171+
"\n",
172+
"❗️ 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."
173+
]
174+
},
175+
{
176+
"cell_type": "code",
177+
"execution_count": null,
178+
"metadata": {},
179+
"outputs": [],
180+
"source": [
181+
"import utils\n",
182+
"import json\n",
183+
"from apimrequests import ApimRequests\n",
184+
"from apimtesting import ApimTesting\n",
185+
"from users import UserHelper\n",
186+
"from authfactory import AuthFactory\n",
187+
"\n",
188+
"tests = ApimTesting(\"OAuth 3rd Party (Spotify) Sample Tests\")\n",
189+
"\n",
190+
"# 1) Marketing Member Role\n",
191+
"# Create a JSON Web Token with a payload and sign it with the symmetric key from above.\n",
192+
"encoded_jwt_token_marketing_member = AuthFactory.create_symmetric_jwt_token_for_user(UserHelper.get_user_by_role(Role.MARKETING_MEMBER), jwt_key_value)\n",
193+
"print(f'\\nJWT token for Marketing Member:\\n{encoded_jwt_token_marketing_member}') # this value is used to call the APIs via APIM\n",
194+
"\n",
195+
"# 2) Issue a direct request to API Management\n",
196+
"artist_id = '6XpaIBNiVzIetEPCWDvAFP' # Whitney Houston's Spotify Artist ID\n",
197+
"reqsApim = ApimRequests(apim_gateway_url)\n",
198+
"reqsApim.headers['Authorization'] = f'Bearer {encoded_jwt_token_marketing_member}'\n",
199+
"\n",
200+
"output = reqsApim.singleGet(f'/oauth-3rd-party-spotify/artists/{artist_id}', msg = 'Calling the Spotify Artist API via API Management Gateway URL. Response codes 200 and 403 are both valid depending on the infrastructure used.')\n",
201+
"artist = json.loads(output)\n",
202+
"tests.verify(artist['name'], 'Whitney Houston')\n",
203+
"utils.print_info(f'{artist[\"name\"]} has a popularity rating of {artist[\"popularity\"]} with {artist[\"followers\"][\"total\"]:,} followers on Spotify.')\n",
204+
"\n",
205+
"# 2) Issue requests against Front Door.\n",
206+
"# Check if the infrastructure architecture deployment uses Azure Front Door.\n",
207+
"utils.print_message('Checking if the infrastructure architecture deployment uses Azure Front Door.', blank_above = True)\n",
208+
"afd_endpoint_url = utils.get_frontdoor_url(deployment, rg_name)\n",
209+
"\n",
210+
"if afd_endpoint_url:\n",
211+
" artist_id = '2VSHKHBTiXWplO8lxcnUC9' # Taylor Swift's Spotify Artist ID\n",
212+
" reqsAfd = ApimRequests(afd_endpoint_url)\n",
213+
" reqsAfd.headers['Authorization'] = f'Bearer {encoded_jwt_token_marketing_member}'\n",
214+
" output = reqsAfd.singleGet(f'/oauth-3rd-party-spotify/artists/{artist_id}', msg = 'Calling the Spotify Artist API via API Management Gateway URL. Response codes 200 and 403 are both valid depending on the infrastructure used.')\n",
215+
" artist = json.loads(output)\n",
216+
" tests.verify(artist['name'], 'Whitney Houston')\n",
217+
" utils.print_info(f'{artist[\"name\"]} has a popularity rating of {artist[\"popularity\"]} with {artist[\"followers\"][\"total\"]:,} followers on Spotify.')\n",
218+
"\n",
219+
"tests.print_summary()\n",
220+
"\n",
221+
"utils.print_ok('All done!')"
222+
]
223+
}
224+
],
225+
"metadata": {
226+
"kernelspec": {
227+
"display_name": "APIM Samples Python 3.12",
228+
"language": "python",
229+
"name": "apim-samples"
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

Comments
 (0)