Skip to content

Commit f6307cb

Browse files
acoumbAndyButland
andauthored
OAuth1a authorization feature implementation (#28)
* OAuth1a authorization feature implementation * Updated docs * Update src/Umbraco.AuthorizedServices/Controllers/AuthorizedServiceResponseController.cs Co-authored-by: Andy Butland <[email protected]> * OAuth1 refactoring and usings. * Add OAuth1 unit tests * Update OAuth1 generate request token flow. * Added details of callback URLs to readme. * Refactored method names. Removed a custom cache class and used Umbraco's runtime cache. Fixed issues with migrations. Added a migration to rename the original table name now that we have different types of tokens. Removed an unused method from IAuthorizedRequestBuilder. Aligned use of cache for tokens for OAuth1 with OAuth2. Bumped version to 0.3. * Update callback URLs * OAuth1 request access token flow updates * Removed unused method and refactored creation of request message for the different authentication methods. * Refactored controller method names. * Removed UseRequestTokenWithExtendedParametersList and used extended list for all OAuth1 requests. * Updated test web app settings. * Updated tests. --------- Co-authored-by: Andy Butland <[email protected]>
1 parent f434466 commit f6307cb

File tree

63 files changed

+1662
-354
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

63 files changed

+1662
-354
lines changed

README.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,15 @@ Install-Package Umbraco.AuthorizedServices
4545
dotnet add package Umbraco.AuthorizedServices
4646
```
4747

48-
4948
### App Creation
5049

5150
Services that this package are intended to support will offer an OAuth authentication and authorization flow against an "app" that the developer will need to create with the service. From this various information will be available, including for example a "client ID" and "client secret" that will need to be applied in configuration.
5251

52+
When creating the app it will usually be necessary to configure a call back URL. You should use the following:
53+
54+
- For OAuth2: `/umbraco/api/AuthorizedServiceResponse/HandleOAuth2IdentityResponse`
55+
- For OAuth1: `/umbraco/api/AuthorizedServiceResponse/HandleOAuth1IdentityResponse`
56+
5357
### Configuring a Service
5458

5559
Details of services available need to be applied to the Umbraco web application's configuration, which, if using the `appSettings.json` file, will look as follows. Other sources such as environment variables can also be used, as per standard .NET configuration.
@@ -149,7 +153,7 @@ An enum value that defines the JSON serializer to use when creating requests and
149153

150154
###### AuthorizationRequestRequiresAuthorizationHeaderWithBasicToken
151155

152-
This flag indicates whether the basic token should be included in the request for access token. If true, a base64 encoding of <clientId>:<clientSecret> will be added to
156+
This flag indicates whether the basic token should be included in the request for access token. If true, a base64 encoding of <clientId>:<clientSecret> will be added to
153157
the authorization header.
154158

155159
###### ClientId *

examples/Umbraco.AuthorizedServices.TestSite/Controllers/TestAuthorizedServicesController.cs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,12 +135,23 @@ public IActionResult GetApiKey(string serviceAlias)
135135

136136
public IActionResult GetAccessToken(string serviceAlias)
137137
{
138-
var response = _authorizedServiceCaller.GetToken(serviceAlias);
138+
var response = _authorizedServiceCaller.GetOAuth2AccessToken(serviceAlias);
139139
if (response == null)
140140
{
141141
return Problem("Could not retrieve access token.");
142142
}
143143

144144
return Content(response);
145145
}
146+
147+
public IActionResult GetOAuth1OAuthToken(string serviceAlias)
148+
{
149+
var response = _authorizedServiceCaller.GetOAuth1Token(serviceAlias);
150+
if (response == null)
151+
{
152+
return Problem("Could not retrieve the OAuth token.");
153+
}
154+
155+
return Content(response);
156+
}
146157
}

examples/Umbraco.AuthorizedServices.TestSite/appsettings.json

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -154,8 +154,9 @@
154154
"Scopes": "r_emailaddress r_liteprofile w_member_social",
155155
"SampleRequest": "/v2/me"
156156
},
157-
"twitter": {
158-
"DisplayName": "Twitter",
157+
"twitter-oauth2": {
158+
"DisplayName": "Twitter OAuth2",
159+
"AuthenticationMethod": "OAuth2Code",
159160
"ApiHost": "https://api.twitter.com",
160161
"IdentityHost": "https://twitter.com",
161162
"TokenHost": "https://api.twitter.com",
@@ -169,6 +170,38 @@
169170
"Scopes": "offline.access tweet.read users.read",
170171
"SampleRequest": "/2/users/me"
171172
},
173+
"twitter": {
174+
"DisplayName": "Twitter",
175+
"AuthenticationMethod": "OAuth1",
176+
"ApiHost": "https://api.twitter.com",
177+
"IdentityHost": "https://api.twitter.com",
178+
"TokenHost": "https://api.twitter.com",
179+
"RequestIdentityPath": "/oauth/authorize",
180+
"RequestTokenPath": "/oauth/access_token",
181+
"RequestTokenFormat": "FormUrlEncoded",
182+
"RequestAuthorizationPath": "/oauth/request_token",
183+
"AuthorizationUrlRequiresRedirectUrl": false,
184+
"ClientId": "",
185+
"ClientSecret": "",
186+
"Scopes": "",
187+
"SampleRequest": "/1.1/account/settings.json"
188+
},
189+
"flickr": {
190+
"DisplayName": "Flickr",
191+
"AuthenticationMethod": "OAuth1",
192+
"ApiHost": "https://www.flickr.com/services",
193+
"IdentityHost": "https://www.flickr.com/services",
194+
"TokenHost": "https://www.flickr.com/services",
195+
"RequestAuthorizationPath": "/oauth/request_token",
196+
"RequestIdentityPath": "/oauth/authorize",
197+
"RequestTokenPath": "/oauth/access_token",
198+
"RequestTokenFormat": "Querystring",
199+
"AuthorizationUrlRequiresRedirectUrl": true,
200+
"ClientId": "",
201+
"ClientSecret": "",
202+
"Scopes": "",
203+
"SampleRequest": "/rest?nojsoncallback=1&format=json&method=flickr.test.login"
204+
},
172205
"facebook": {
173206
"DisplayName": "Facebook",
174207
"ApiHost": "https://graph.facebook.com",

src/Umbraco.AuthorizedServices/AuthorizedServicesComposer.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using Microsoft.Extensions.DependencyInjection;
22
using Umbraco.AuthorizedServices.Configuration;
33
using Umbraco.AuthorizedServices.Manifests;
4+
using Umbraco.AuthorizedServices.Models;
45
using Umbraco.AuthorizedServices.Services;
56
using Umbraco.AuthorizedServices.Services.Implement;
67
using Umbraco.Cms.Core.Composing;
@@ -51,15 +52,17 @@ private static void RegisterServices(IUmbracoBuilder builder)
5152
builder.Services.AddUnique<IDateTimeProvider, DateTimeProvider>();
5253
builder.Services.AddUnique<IRefreshTokenParametersBuilder, RefreshTokenParametersBuilder>();
5354

54-
builder.Services.AddUnique<IAuthorizationPayloadCache, AuthorizationPayloadCache>();
5555
builder.Services.AddUnique<IAuthorizationPayloadBuilder, AuthorizationPayloadBuilder>();
5656

5757
builder.Services.AddUnique<ISecretEncryptor, DataProtectionSecretEncryptor>();
5858

5959
builder.Services.AddUnique<ITokenFactory, TokenFactory>();
60-
builder.Services.AddUnique<ITokenStorage, DatabaseTokenStorage>();
60+
builder.Services.AddUnique<IOAuth2TokenStorage, DatabaseOAuth2TokenStorage>();
61+
builder.Services.AddUnique<IOAuth1TokenStorage, DatabaseOAuth1TokenStorage>();
6162
builder.Services.AddUnique<IKeyStorage, DatabaseKeyStorage>();
6263

6364
builder.Services.AddSingleton<JsonSerializerFactory>();
65+
66+
builder.Services.AddHttpContextAccessor();
6467
}
6568
}

src/Umbraco.AuthorizedServices/ClientApp/src/backoffice/AuthorizedServices/edit.controller.ts

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,24 +12,36 @@ function AuthorizedServiceEditController(this: any, $routeParams, $location, aut
1212
vm.displayName = serviceData.displayName;
1313
vm.headerName = "Authorized Services: " + vm.displayName;
1414
vm.isAuthorized = serviceData.isAuthorized;
15-
vm.authenticationMethod = serviceData.authenticationMethod;
1615
vm.canManuallyProvideToken = serviceData.canManuallyProvideToken;
1716
vm.canManuallyProvideApiKey = serviceData.canManuallyProvideApiKey;
1817
vm.authorizationUrl = serviceData.authorizationUrl;
1918
vm.sampleRequest = serviceData.sampleRequest;
2019
vm.sampleRequestResponse = null;
2120
vm.settings = serviceData.settings;
22-
vm.isOAuthBasedAuthenticationMethod = serviceData.authenticationMethod !== AuthenticationMethod.ApiKey;
21+
22+
vm.authenticationMethod = {
23+
isOAuth1: serviceData.authenticationMethod === AuthenticationMethod.OAuth1,
24+
isOAuth2AuthorizationCode: serviceData.authenticationMethod === AuthenticationMethod.OAuth2AuthorizationCode,
25+
isOAuth2ClientCredentials: serviceData.authenticationMethod === AuthenticationMethod.OAuth2ClientCredentials,
26+
isApiKey: serviceData.authenticationMethod === AuthenticationMethod.ApiKey
27+
};
28+
}, function (ex) {
29+
notificationsService.error("Authorized Services", ex.data.ExceptionMessage);
2330
});
2431
}
2532

2633
vm.authorizeAccess = function () {
27-
if (vm.authenticationMethod.toString() === AuthenticationMethod.OAuth2ClientCredentials.toString()) {
28-
authorizedServiceResource.generateToken(serviceAlias)
34+
if (vm.authenticationMethod.isOAuth2ClientCredentials) {
35+
authorizedServiceResource.generateOAuth2ClientCredentialsToken(serviceAlias)
2936
.then(function () {
3037
notificationsService.success("Authorized Services", "The '" + vm.displayName + "' service has been authorized.");
3138
loadServiceDetails(serviceAlias);
3239
});
40+
} if (vm.authenticationMethod.isOAuth1) {
41+
authorizedServiceResource.generateOAuth1RequestToken(serviceAlias)
42+
.then(function (response) {
43+
location.href = response.data.message;
44+
});
3345
}
3446
else {
3547
location.href = vm.authorizationUrl;
@@ -46,7 +58,7 @@ function AuthorizedServiceEditController(this: any, $routeParams, $location, aut
4658

4759
vm.sendSampleRequest = function () {
4860
vm.sampleRequestResponse = null;
49-
authorizedServiceResource.sendSampleRequest(serviceAlias, vm.sampleRequest)
61+
authorizedServiceResource.sendSampleRequest(serviceAlias)
5062
.then(function (response) {
5163
vm.sampleRequestResponse = "Request: " + vm.sampleRequest + "\r\nResponse: " + JSON.stringify(response.data, null, 2);
5264
})
@@ -55,11 +67,11 @@ function AuthorizedServiceEditController(this: any, $routeParams, $location, aut
5567
});
5668
};
5769

58-
vm.saveAccessToken = function () {
70+
vm.saveOAuth2AccessToken = function () {
5971
let inAccessToken = <HTMLInputElement>document.getElementById("inAccessToken");
6072

6173
if (inAccessToken) {
62-
authorizedServiceResource.saveToken(serviceAlias, inAccessToken.value)
74+
authorizedServiceResource.saveOAuth2Token(serviceAlias, inAccessToken.value)
6375
.then(function () {
6476
notificationsService.success("Authorized Services", "The '" + vm.displayName + "' service access token has been saved.");
6577
inAccessToken.value = "";
@@ -68,6 +80,21 @@ function AuthorizedServiceEditController(this: any, $routeParams, $location, aut
6880
}
6981
}
7082

83+
vm.saveOAuth1TokenDetails = function () {
84+
let inAccessToken = <HTMLInputElement>document.getElementById("inAccessToken");
85+
let inTokenSecret = <HTMLInputElement>document.getElementById("inTokenSecret");
86+
87+
if (inAccessToken && inTokenSecret) {
88+
authorizedServiceResource.saveOAuth1Token(serviceAlias, inAccessToken.value, inTokenSecret.value)
89+
.then(function () {
90+
notificationsService.success("Authorized Services", "The '" + vm.displayName + "' service access token and token secret have been saved.");
91+
inAccessToken.value = "";
92+
inTokenSecret.value = "";
93+
loadServiceDetails(serviceAlias);
94+
});
95+
}
96+
}
97+
7198
vm.saveApiKey = function () {
7299
let inApiKey = <HTMLInputElement>document.getElementById("inApiKey");
73100

src/Umbraco.AuthorizedServices/ClientApp/src/backoffice/AuthorizedServices/edit.html

Lines changed: 59 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,7 @@
2424
action="vm.sendSampleRequest()"
2525
type="button"
2626
label="Verify Sample Request"></umb-button>
27-
<umb-button ng-if="vm.isOAuthBasedAuthenticationMethod
28-
|| (!vm.isOAuthBasedAuthenticationMethod && vm.canManuallyProvideApiKey)"
27+
<umb-button ng-if="vm.authenticationMethod.isOAuth1 || vm.authenticationMethod.isOAuth2AuthorizationCode || vm.authenticationMethod.isOAuth2ClientCredentials"
2928
action="vm.revokeAccess()"
3029
type="button"
3130
button-style="danger"
@@ -48,8 +47,39 @@
4847
</uui-icon-registry-essential>
4948
</umb-box-content>
5049
</umb-box>
51-
<!-- Provide Token Section -->
52-
<umb-box ng-if="vm.canManuallyProvideToken">
50+
<!-- Provide OAuth1 Token Section -->
51+
<umb-box ng-if="vm.canManuallyProvideToken && vm.authenticationMethod.isOAuth1">
52+
<umb-box-header title="Provide OAuth1 Access Token and Token Secret"></umb-box-header>
53+
<umb-box-content>
54+
<uui-icon-registry-essential>
55+
<uui-card-content-node name="OAuth1 Access Token and Token Secret">
56+
<uui-icon slot="icon" name="add"></uui-icon>
57+
<p class="auth-srv">Enter service access token and token secret</p>
58+
<p>This service is configured indicating that an access token and a token secret can be generated via the service's developer portal. Once you have obtained them you can copy and paste them here to authorize the service.</p>
59+
<div>
60+
<uui-form>
61+
<uui-form-layout-item class="oauth-form-item">
62+
<uui-label for="inAccessToken" slot="label">Access Token</uui-label>
63+
<uui-input id="inAccessToken" name="access_token" type="text" label="Access Token"></uui-input>
64+
</uui-form-layout-item>
65+
<uui-form-layout-item class="oauth-form-item">
66+
<uui-label for="inTokenSecret" slot="label">Token Secret</uui-label>
67+
<uui-input id="inTokenSecret" name="token_secret" type="text" label="Token Secret"></uui-input>
68+
</uui-form-layout-item>
69+
<div>
70+
<umb-button action="vm.saveOAuth1TokenDetails()"
71+
type="button"
72+
button-style="primary"
73+
label="Save"></umb-button>
74+
</div>
75+
</uui-form>
76+
</div>
77+
</uui-card-content-node>
78+
</uui-icon-registry-essential>
79+
</umb-box-content>
80+
</umb-box>
81+
<!-- Provide OAuth2 Token Section -->
82+
<umb-box ng-if="vm.canManuallyProvideToken && (vm.authenticationMethod.isOAuth2AuthorizationCode || vm.authenticationMethod.isOAuth2ClientCredentials)">
5383
<umb-box-header title="Provide Token"></umb-box-header>
5484
<umb-box-content>
5585
<uui-icon-registry-essential>
@@ -58,18 +88,25 @@
5888
<p class="auth-srv">Enter service access token</p>
5989
<p>This service is configured indicating that a token can be generated via the service's developer portal. Once you have obtained one you can copy and paste it here to authorize the service.</p>
6090
<div>
61-
<uui-input id="inAccessToken" type="text" name="inAccessToken" style="width: 40%;font-size:14px;"></uui-input>
62-
<umb-button action="vm.saveAccessToken()"
63-
type="button"
64-
button-style="primary"
65-
label="Save"></umb-button>
91+
<uui-form>
92+
<uui-form-layout-item class="oauth-form-item">
93+
<uui-label for="inAccessToken" slot="label">Access Token</uui-label>
94+
<uui-input id="inAccessToken" name="access_token" type="text" label="Access Token"></uui-input>
95+
</uui-form-layout-item>
96+
<div>
97+
<umb-button action="vm.saveOAuth2AccessToken()"
98+
type="button"
99+
button-style="primary"
100+
label="Save"></umb-button>
101+
</div>
102+
</uui-form>
66103
</div>
67104
</uui-card-content-node>
68105
</uui-icon-registry-essential>
69106
</umb-box-content>
70107
</umb-box>
71108
<!-- Provide Key Section -->
72-
<umb-box ng-if="vm.canManuallyProvideApiKey">
109+
<umb-box ng-if="vm.canManuallyProvideApiKey && vm.authenticationMethod.isApiKey">
73110
<umb-box-header title="Provide API key"></umb-box-header>
74111
<umb-box-content>
75112
<uui-icon-registry-essential>
@@ -81,11 +118,18 @@
81118
Once you have obtained one you can copy and paste it here to authorize the service.
82119
</p>
83120
<div>
84-
<uui-input id="inApiKey" type="text" name="inApiKey" style="width: 40%;font-size:14px;"></uui-input>
85-
<umb-button action="vm.saveApiKey()"
86-
type="button"
87-
button-style="primary"
88-
label="Save"></umb-button>
121+
<uui-form>
122+
<uui-form-layout-item class="oauth-form-item">
123+
<uui-label for="inApiKey" slot="label">Api Key</uui-label>
124+
<uui-input id="inApiKey" name="access_token" type="text" label="Api Key"></uui-input>
125+
</uui-form-layout-item>
126+
<div>
127+
<umb-button action="vm.saveApiKey()"
128+
type="button"
129+
button-style="primary"
130+
label="Save"></umb-button>
131+
</div>
132+
</uui-form>
89133
</div>
90134
</uui-card-content-node>
91135
</uui-icon-registry-essential>

src/Umbraco.AuthorizedServices/ClientApp/src/css/style.css

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,12 @@
3232
padding: 4px 8px;
3333
}
3434

35+
.oauth-form-item uui-label {
36+
font-size: 14px;
37+
}
38+
39+
.oauth-form-item uui-input {
40+
font-size: 14px;
41+
width: 40%;
42+
}
43+

src/Umbraco.AuthorizedServices/ClientApp/src/resources/authorizedservice.resource.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,26 @@ function authorizedServiceResource($q, $http) {
77
getByAlias: function (alias: string) {
88
return $http.get(apiRoot + "GetByAlias?alias=" + alias);
99
},
10-
11-
sendSampleRequest: function (alias: string, path: string) {
12-
return $http.get(apiRoot + "SendSampleRequest?alias=" + alias + "&path=" + path);
10+
sendSampleRequest: function (alias: string) {
11+
return $http.get(apiRoot + "SendSampleRequest?alias=" + alias);
1312
},
1413
revokeAccess: function (alias: string) {
1514
return $http.post(apiRoot + "RevokeAccess", { alias: alias });
1615
},
17-
saveToken: function (alias: string, token: string) {
18-
return $http.post(apiRoot + "SaveToken", { alias: alias, token: token });
16+
saveOAuth2Token: function (alias: string, token: string) {
17+
return $http.post(apiRoot + "SaveOAuth2Token", { alias: alias, token: token });
18+
},
19+
saveOAuth1Token: function (alias: string, token: string, tokenSecret: string) {
20+
return $http.post(apiRoot + "SaveOAuth1Token", { alias: alias, token: token, tokenSecret: tokenSecret });
1921
},
2022
saveApiKey: function (alias: string, apiKey: string) {
2123
return $http.post(apiRoot + "SaveApiKey", { alias: alias, apiKey: apiKey });
2224
},
23-
generateToken: function (alias: string) {
24-
return $http.post(apiRoot + "GenerateToken", { alias: alias });
25+
generateOAuth2ClientCredentialsToken: function (alias: string) {
26+
return $http.post(apiRoot + "GenerateOAuth2ClientCredentialsToken", { alias: alias });
27+
},
28+
generateOAuth1RequestToken: function (alias: string) {
29+
return $http.post(apiRoot + "GenerateOAuth1RequestToken", { alias: alias});
2530
}
2631
};
2732
}

src/Umbraco.AuthorizedServices/Configuration/AuthorizedServiceSettings.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,11 @@ public class ServiceDetail : ServiceSummary
163163
/// </summary>
164164
public bool CanManuallyProvideApiKey { get; set; }
165165

166+
/// <summary>
167+
/// Gets or sets the path for requests for obtaining authorization for a user in OAuth1 flows.
168+
/// </summary>
169+
public string RequestAuthorizationPath { get; set; } = string.Empty;
170+
166171
/// <summary>
167172
/// Gets or sets the path for requests for authentication with the service.
168173
/// </summary>
@@ -178,6 +183,11 @@ public class ServiceDetail : ServiceSummary
178183
/// </summary>
179184
public string RequestTokenPath { get; set; } = string.Empty;
180185

186+
/// <summary>
187+
/// Gets or sets the HTTP method used to retrieve the access token.
188+
/// </summary>
189+
public HttpMethod RequestTokenMethod { get; set; } = HttpMethod.Post;
190+
181191
/// <summary>
182192
/// Gets or sets the format to use for encoding the request for a token.
183193
/// </summary>

0 commit comments

Comments
 (0)