Skip to content

Commit ef14c2c

Browse files
authored
Merge pull request #26 from umbraco/feature/long-lived-access-token
Exchange access tokens implementation
2 parents 2871179 + 5bfce63 commit ef14c2c

19 files changed

+327
-25
lines changed

examples/Umbraco.AuthorizedServices.TestSite/appsettings.json

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -432,7 +432,7 @@
432432
"RequestIdentityPath": "",
433433
"RequestTokenPath": "",
434434
"RequestTokenFormat": "",
435-
"ApiKey: "[api_key]",
435+
"ApiKey": "",
436436
"ApiKeyProvision": {
437437
"Method": "QueryString",
438438
"Key": "key"
@@ -472,6 +472,30 @@
472472
"Scopes": "https://graph.microsoft.com/.default",
473473
"IncludeScopesInAuthorizationRequest": true,
474474
"SampleRequest": "/me"
475+
},
476+
"instagram": {
477+
"DisplayName": "Instagram",
478+
"ApiHost": "https://api.instagram.com",
479+
"IdentityHost": "https://api.instagram.com",
480+
"TokenHost": "https://api.instagram.com",
481+
"RequestIdentityPath": "/oauth/authorize",
482+
"RequestTokenPath": "/oauth/access_token",
483+
"RequestTokenFormat": "FormUrlEncoded",
484+
"AuthorizationUrlRequiresRedirectUrl": true,
485+
"ClientId": "",
486+
"ClientSecret": "",
487+
"Scopes": "user_profile",
488+
"SampleRequest": "/v3.0/me",
489+
"CanExchangeToken": true,
490+
"ExchangeTokenProvision": {
491+
"TokenHost": "https://graph.instagram.com",
492+
"RequestTokenPath": "/access_token",
493+
"TokenGrantType": "ig_exchange_token",
494+
"RequestRefreshTokenPath": "/refresh_access_token",
495+
"RefreshTokenGrantType": "ig_refresh_token",
496+
"ExchangeTokenWhenExpiresWithin": "25.00:00:00"
497+
},
498+
"RefreshAccessTokenWhenExpiresWithin": "00.00:00:40"
475499
}
476500
}
477501
}

src/Umbraco.AuthorizedServices/AuthorizedServicesComposer.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ private static void RegisterServices(IUmbracoBuilder builder)
4141
{
4242
builder.Services.AddUnique<IAuthorizationClientFactory, AuthorizationClientFactory>();
4343
builder.Services.AddUnique<IAuthorizationParametersBuilder, AuthorizationParametersBuilder>();
44+
builder.Services.AddUnique<IExchangeTokenParametersBuilder, ExchangeTokenParametersBuilder>();
4445
builder.Services.AddUnique<IAuthorizationRequestSender, AuthorizationRequestSender>();
4546
builder.Services.AddUnique<IAuthorizedServiceAuthorizer, AuthorizedServiceAuthorizer>();
4647
builder.Services.AddUnique<IAuthorizationUrlBuilder, AuthorizationUrlBuilder>();

src/Umbraco.AuthorizedServices/Configuration/AuthorizedServiceSettings.cs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,27 @@ public class ApiKeyProvision
8383
public override string ToString() => $"{Method} / {Key}";
8484
}
8585

86+
/// <summary>
87+
/// Defines the provisioning options for an API exchanging short lived tokens with long lived ones.
88+
/// </summary>
89+
public class ExchangeTokenProvision
90+
{
91+
public string TokenHost { get; set;} = string.Empty;
92+
93+
public string RequestTokenPath { get; set;} = string.Empty;
94+
95+
public string TokenGrantType { get; set; } = string.Empty;
96+
97+
public string RequestRefreshTokenPath { get; set; } = string.Empty;
98+
99+
public string RefreshTokenGrantType { get; set; } = string.Empty;
100+
101+
/// <summary>
102+
/// Gets or sets the time interval for expiration of exchange tokens.
103+
/// </summary>
104+
public TimeSpan ExchangeTokenWhenExpiresWithin { get; set; } = TimeSpan.FromDays(30);
105+
}
106+
86107
/// <summary>
87108
/// Defines the strongly typed configuration for a single service.
88109
/// </summary>
@@ -207,6 +228,16 @@ public class ServiceDetail : ServiceSummary
207228
/// </summary>
208229
public bool UseProofKeyForCodeExchange { get; set; }
209230

231+
/// <summary>
232+
/// Gets or sets a value indicating whether the OAuth flow should exchange the access token.
233+
/// </summary>
234+
public bool CanExchangeToken { get; set; }
235+
236+
/// <summary>
237+
/// Gets or sets the provisioning options for exchanging token flow.
238+
/// </summary>
239+
public ExchangeTokenProvision? ExchangeTokenProvision { get; set; }
240+
210241
/// <summary>
211242
/// Gets or sets the key expected in the token response that identifies the access token.
212243
/// </summary>
@@ -227,6 +258,11 @@ public class ServiceDetail : ServiceSummary
227258
/// </summary>
228259
public string? SampleRequest { get; set; }
229260

261+
/// <summary>
262+
/// Gets or sets the time interval for expiration of access tokens.
263+
/// </summary>
264+
public TimeSpan RefreshAccessTokenWhenExpiresWithin { get; set; } = TimeSpan.FromSeconds(30);
265+
230266
internal string GetTokenHost() => string.IsNullOrWhiteSpace(TokenHost)
231267
? IdentityHost
232268
: TokenHost;

src/Umbraco.AuthorizedServices/Controllers/AuthorizedServiceResponseController.cs

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
using Microsoft.AspNetCore.Mvc;
2+
using Microsoft.Extensions.Options;
3+
using Umbraco.AuthorizedServices.Configuration;
24
using Umbraco.AuthorizedServices.Exceptions;
35
using Umbraco.AuthorizedServices.Extensions;
46
using Umbraco.AuthorizedServices.Models;
@@ -14,16 +16,19 @@ public class AuthorizedServiceResponseController : UmbracoApiController
1416
{
1517
private readonly IAuthorizedServiceAuthorizer _serviceAuthorizer;
1618
private readonly IAuthorizationPayloadCache _authorizedServiceAuthorizationPayloadCache;
19+
private readonly IOptionsMonitor<ServiceDetail> _serviceDetailOptions;
1720

1821
/// <summary>
1922
/// Initializes a new instance of the <see cref="AuthorizedServiceResponseController"/> class.
2023
/// </summary>
2124
public AuthorizedServiceResponseController(
2225
IAuthorizedServiceAuthorizer serviceAuthorizer,
23-
IAuthorizationPayloadCache authorizedServiceAuthorizationPayloadCache)
26+
IAuthorizationPayloadCache authorizedServiceAuthorizationPayloadCache,
27+
IOptionsMonitor<ServiceDetail> serviceDetailOptions)
2428
{
2529
_serviceAuthorizer = serviceAuthorizer;
2630
_authorizedServiceAuthorizationPayloadCache = authorizedServiceAuthorizationPayloadCache;
31+
_serviceDetailOptions = serviceDetailOptions;
2732
}
2833

2934
/// <summary>
@@ -52,11 +57,39 @@ public async Task<IActionResult> HandleIdentityResponse(string code, string stat
5257
_authorizedServiceAuthorizationPayloadCache.Remove(stateParts[0]);
5358
AuthorizationResult result = await _serviceAuthorizer.AuthorizeOAuth2AuthorizationCodeServiceAsync(serviceAlias, code, redirectUri, codeVerifier);
5459

60+
// handle exchange
61+
ServiceDetail serviceDetail = _serviceDetailOptions.Get(serviceAlias);
62+
if (serviceDetail.CanExchangeToken)
63+
{
64+
return await HandleTokenExchange(serviceDetail);
65+
}
66+
5567
if (result.Success)
5668
{
5769
return Redirect($"/umbraco#/settings/AuthorizedServices/edit/{serviceAlias}");
5870
}
5971

6072
throw new AuthorizedServiceException("Failed to obtain access token");
6173
}
74+
75+
/// <summary>
76+
/// Handles the token exchange flow.
77+
/// </summary>
78+
/// <param name="serviceDetail">The service detail.</param>
79+
private async Task<IActionResult> HandleTokenExchange(ServiceDetail serviceDetail)
80+
{
81+
if (serviceDetail.ExchangeTokenProvision is null)
82+
{
83+
throw new AuthorizedServiceException("Failed to retrieve exchange token provisioning.");
84+
}
85+
86+
AuthorizationResult exchangeResult = await _serviceAuthorizer.ExchangeOAuth2AccessTokenAsync(serviceDetail.Alias);
87+
88+
if (exchangeResult.Success)
89+
{
90+
return Redirect($"/umbraco#/settings/AuthorizedServices/edit/{serviceDetail.Alias}");
91+
}
92+
93+
throw new AuthorizedServiceException("Failed to exchange the access token.");
94+
}
6295
}

src/Umbraco.AuthorizedServices/Models/Token.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
using System;
2+
13
namespace Umbraco.AuthorizedServices.Models;
24

35
/// <summary>
@@ -40,7 +42,7 @@ public Token(string accessToken, string? refreshToken, DateTime? expiresOn)
4042
public DateTime? ExpiresOn { get; }
4143

4244
/// <summary>
43-
/// Checks to see if the token either has or is about to expire (in the next 30 seconds).
45+
/// Checks to see if the token will be expired after the provided period.
4446
/// </summary>
45-
public bool HasOrIsAboutToExpire => ExpiresOn.HasValue && DateTime.UtcNow.AddSeconds(30) > ExpiresOn;
47+
public bool WillBeExpiredAfter(TimeSpan period) => ExpiresOn.HasValue && DateTime.UtcNow.Add(period) > ExpiresOn;
4648
}

src/Umbraco.AuthorizedServices/Services/IAuthorizationRequestSender.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,12 @@ public interface IAuthorizationRequestSender
1414
/// <param name="parameters">The authorization parameters.</param>
1515
/// <returns>A <see cref="Task{HttpResponseMessage}"/> representing the result of the asynchronous operation.</returns>
1616
Task<HttpResponseMessage> SendRequest(ServiceDetail serviceDetail, Dictionary<string, string> parameters);
17+
18+
/// <summary>
19+
/// Sends an authorization request to a service.
20+
/// </summary>
21+
/// <param name="serviceDetail">The service detail.</param>
22+
/// <param name="parameters">The authorization parameters.</param>
23+
/// <returns>A <see cref="Task{HttpResponseMessage}"/> representing the result of the asynchronous operation.</returns>
24+
Task<HttpResponseMessage> SendExchangeRequest(ServiceDetail serviceDetail, Dictionary<string, string> parameters);
1725
}

src/Umbraco.AuthorizedServices/Services/IAuthorizedServiceAuthorizer.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,11 @@ public interface IAuthorizedServiceAuthorizer
2323
/// <param name="serviceAlias">The service alias.</param>
2424
/// <returns>A <see cref="Task{AuthorizationResult}"/> representing the result of the asynchronous operation.</returns>
2525
Task<AuthorizationResult> AuthorizeOAuth2ClientCredentialsServiceAsync(string serviceAlias);
26+
27+
/// <summary>
28+
/// Exchanges the access token with a long lived one.
29+
/// </summary>
30+
/// <param name="serviceAlias">The service alias.</param>
31+
/// <returns>A <see cref="Task{AuthorizationResult}"/> representing the result of the asynchronous operation.</returns>
32+
Task<AuthorizationResult> ExchangeOAuth2AccessTokenAsync(string serviceAlias);
2633
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
using Umbraco.AuthorizedServices.Configuration;
2+
3+
namespace Umbraco.AuthorizedServices.Services;
4+
5+
/// <summary>
6+
/// Defines operations on building the parameter dictionary used in exchange token authorization requests.
7+
/// </summary>
8+
public interface IExchangeTokenParametersBuilder
9+
{
10+
/// <summary>
11+
/// Builds the parameter dictionary used in the exchange token flow.
12+
/// </summary>
13+
/// <param name="serviceDetail">The service detail.</param>
14+
/// <param name="accessToken">The short lived access token.</param>
15+
/// <returns>A dictionary containing the authorization parameters.</returns>
16+
Dictionary<string, string> BuildParameters(ServiceDetail serviceDetail, string accessToken);
17+
}

src/Umbraco.AuthorizedServices/Services/IRefreshTokenParametersBuilder.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ namespace Umbraco.AuthorizedServices.Services;
88
public interface IRefreshTokenParametersBuilder
99
{
1010
/// <summary>
11-
/// Builds the the parameter dictionary used in token refresh requests.
11+
/// Builds the parameter dictionary used in token refresh requests.
1212
/// </summary>
1313
/// <param name="serviceDetail">The service detail.</param>
1414
/// <param name="refreshToken">The refresh token.</param>

src/Umbraco.AuthorizedServices/Services/Implement/AuthorizationRequestSender.cs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,31 @@ public async Task<HttpResponseMessage> SendRequest(ServiceDetail serviceDetail,
3939
return await httpClient.PostAsync(url, content);
4040
}
4141

42+
public async Task<HttpResponseMessage> SendExchangeRequest(ServiceDetail serviceDetail, Dictionary<string, string> parameters)
43+
{
44+
HttpClient httpClient = _authorizationClientFactory.CreateClient();
45+
46+
var url = serviceDetail.ExchangeTokenProvision is not null
47+
? string.Format(
48+
"{0}{1}",
49+
string.IsNullOrWhiteSpace(serviceDetail.ExchangeTokenProvision.TokenHost)
50+
? serviceDetail.GetTokenHost()
51+
: serviceDetail.ExchangeTokenProvision.TokenHost,
52+
string.IsNullOrWhiteSpace(serviceDetail.ExchangeTokenProvision.RequestTokenPath)
53+
? serviceDetail.RequestTokenPath
54+
: serviceDetail.ExchangeTokenProvision.RequestTokenPath)
55+
: serviceDetail.GetTokenHost() + serviceDetail.RequestTokenPath;
56+
57+
if (serviceDetail.ExchangeTokenProvision is null)
58+
{
59+
throw new ArgumentNullException(nameof(serviceDetail.ExchangeTokenProvision));
60+
}
61+
62+
url += BuildAuthorizationQuerystring(parameters);
63+
64+
return await httpClient.GetAsync(url);
65+
}
66+
4267
private static string BuildAuthorizationQuerystring(Dictionary<string, string> parameters)
4368
{
4469
var qs = new StringBuilder();

0 commit comments

Comments
 (0)