Skip to content

Commit 460f2ef

Browse files
committed
Updates to OAuth sample
Update OAuth web sample
1 parent 711ec0c commit 460f2ef

File tree

8 files changed

+189
-147
lines changed

8 files changed

+189
-147
lines changed

OAuthWebSample/.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
*.user
77
*.sln.docstates
88
*.sln.ide/
9-
OAuthSample/Properties/PublishProfiles/
9+
OAuthWebSample/Properties/PublishProfiles/
1010

1111
# Build results
1212

Lines changed: 153 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -1,145 +1,205 @@
11
using System;
22
using System.Configuration;
3+
using System.Collections.Generic;
34
using System.IO;
45
using System.Net;
56
using System.Web;
67
using System.Web.Mvc;
78
using Newtonsoft.Json;
89
using OAuthSample.Models;
10+
using System.Net.Http;
11+
using System.Threading;
12+
using System.Threading.Tasks;
13+
using Newtonsoft.Json.Linq;
14+
using System.Net.Http.Headers;
915

1016
namespace OAuthSample.Controllers
1117
{
1218
public class OAuthController : Controller
1319
{
14-
//
15-
// GET: /OAuth/
16-
public ActionResult Index()
20+
private static readonly HttpClient s_httpClient = new HttpClient();
21+
private static readonly Dictionary<Guid, TokenModel> s_authorizationRequests = new Dictionary<Guid, TokenModel>();
22+
23+
/// <summary>
24+
/// Start a new authorization request.
25+
///
26+
/// This creates a random state value that is used to correlate/validate the request in the callback later.
27+
/// </summary>
28+
/// <returns></returns>
29+
public ActionResult Authorize()
1730
{
31+
Guid state = Guid.NewGuid();
1832

19-
return View();
33+
s_authorizationRequests[state] = new TokenModel() { IsPending = true };
34+
35+
return new RedirectResult(GetAuthorizationUrl(state.ToString()));
2036
}
2137

22-
public ActionResult RequestToken(string code, string status)
38+
/// <summary>
39+
/// Constructs an authorization URL with the specified state value.
40+
/// </summary>
41+
/// <param name="state"></param>
42+
/// <returns></returns>
43+
private static String GetAuthorizationUrl(String state)
2344
{
24-
return new RedirectResult(GenerateAuthorizeUrl());
45+
UriBuilder uriBuilder = new UriBuilder(ConfigurationManager.AppSettings["AuthUrl"]);
46+
var queryParams = HttpUtility.ParseQueryString(uriBuilder.Query ?? String.Empty);
47+
48+
queryParams["client_id"] = ConfigurationManager.AppSettings["ClientAppId"];
49+
queryParams["response_type"] = "Assertion";
50+
queryParams["state"] = state;
51+
queryParams["scope"] = ConfigurationManager.AppSettings["Scope"];
52+
queryParams["redirect_uri"] = ConfigurationManager.AppSettings["CallbackUrl"];
53+
54+
uriBuilder.Query = queryParams.ToString();
55+
56+
return uriBuilder.ToString();
2557
}
2658

27-
public ActionResult RefreshToken(string refreshToken)
59+
/// <summary>
60+
/// Callback action. Invoked after the user has authorized the app.
61+
/// </summary>
62+
/// <param name="code"></param>
63+
/// <param name="state"></param>
64+
/// <returns></returns>
65+
public async Task<ActionResult> Callback(String code, Guid state)
2866
{
29-
TokenModel token = new TokenModel();
30-
String error = null;
31-
32-
if (!String.IsNullOrEmpty(refreshToken))
67+
String error;
68+
if (ValidateCallbackValues(code, state.ToString(), out error))
3369
{
34-
error = PerformTokenRequest(GenerateRefreshPostData(refreshToken), out token);
35-
if (String.IsNullOrEmpty(error))
70+
// Exchange the auth code for an access token and refresh token
71+
HttpRequestMessage requestMessage = new HttpRequestMessage(HttpMethod.Post, ConfigurationManager.AppSettings["TokenUrl"]);
72+
requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
73+
74+
Dictionary<String, String> form = new Dictionary<String, String>()
3675
{
37-
ViewBag.Token = token;
38-
}
39-
}
76+
{ "client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" },
77+
{ "client_assertion", ConfigurationManager.AppSettings["ClientAppSecret"] },
78+
{ "grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer" },
79+
{ "assertion", code },
80+
{ "redirect_uri", ConfigurationManager.AppSettings["CallbackUrl"] }
81+
};
82+
requestMessage.Content = new FormUrlEncodedContent(form);
4083

41-
ViewBag.Error = error;
84+
HttpResponseMessage responseMessage = await s_httpClient.SendAsync(requestMessage);
4285

43-
return View("TokenView");
44-
}
45-
46-
public ActionResult Callback(string code, string state)
47-
{
48-
TokenModel token = new TokenModel();
49-
String error = null;
86+
if (responseMessage.IsSuccessStatusCode)
87+
{
88+
String body = await responseMessage.Content.ReadAsStringAsync();
5089

51-
if (!String.IsNullOrEmpty(code))
52-
{
53-
error = PerformTokenRequest(GenerateRequestPostData(code), out token);
54-
if (String.IsNullOrEmpty(error))
90+
TokenModel tokenModel = s_authorizationRequests[state];
91+
JsonConvert.PopulateObject(body, tokenModel);
92+
93+
ViewBag.Token = tokenModel;
94+
}
95+
else
5596
{
56-
ViewBag.Token = token;
97+
error = responseMessage.ReasonPhrase;
5798
}
5899
}
59100

60-
ViewBag.Error = error;
101+
if (!String.IsNullOrEmpty(error))
102+
{
103+
ViewBag.Error = error;
104+
}
105+
106+
ViewBag.ProfileUrl = ConfigurationManager.AppSettings["ProfileUrl"];
61107

62108
return View("TokenView");
63109
}
64110

65-
private String PerformTokenRequest(String postData, out TokenModel token)
111+
/// <summary>
112+
/// Ensures the specified auth code and state value are valid. If both are valid, the state value is marked so it can't be used again.
113+
/// </summary>
114+
/// <param name="code"></param>
115+
/// <param name="state"></param>
116+
/// <param name="error"></param>
117+
/// <returns></returns>
118+
private static bool ValidateCallbackValues(String code, String state, out String error)
66119
{
67-
var error = String.Empty;
68-
var strResponseData = String.Empty;
120+
error = null;
69121

70-
HttpWebRequest webRequest = (HttpWebRequest)WebRequest.Create(
71-
ConfigurationManager.AppSettings["TokenUrl"]
72-
);
73-
74-
webRequest.Method = "POST";
75-
webRequest.ContentLength = postData.Length;
76-
webRequest.ContentType = "application/x-www-form-urlencoded";
77-
78-
using (StreamWriter swRequestWriter = new StreamWriter(webRequest.GetRequestStream()))
122+
if (String.IsNullOrEmpty(code))
79123
{
80-
swRequestWriter.Write(postData);
124+
error = "Invalid auth code";
81125
}
82-
83-
try
126+
else
84127
{
85-
HttpWebResponse hwrWebResponse = (HttpWebResponse)webRequest.GetResponse();
86-
87-
if (hwrWebResponse.StatusCode == HttpStatusCode.OK)
128+
Guid authorizationRequestKey;
129+
if (!Guid.TryParse(state, out authorizationRequestKey))
88130
{
89-
using (StreamReader srResponseReader = new StreamReader(hwrWebResponse.GetResponseStream()))
131+
error = "Invalid authorization request key";
132+
}
133+
else
134+
{
135+
TokenModel tokenModel;
136+
if (!s_authorizationRequests.TryGetValue(authorizationRequestKey, out tokenModel))
90137
{
91-
strResponseData = srResponseReader.ReadToEnd();
138+
error = "Unknown authorization request key";
139+
}
140+
else if (!tokenModel.IsPending)
141+
{
142+
error = "Authorization request key already used";
143+
}
144+
else
145+
{
146+
s_authorizationRequests[authorizationRequestKey].IsPending = false; // mark the state value as used so it can't be reused
92147
}
93-
94-
token = JsonConvert.DeserializeObject<TokenModel>(strResponseData);
95-
return null;
96148
}
97149
}
98-
catch (WebException wex)
99-
{
100-
error = "Request Issue: " + wex.Message;
101-
}
102-
catch (Exception ex)
103-
{
104-
error = "Issue: " + ex.Message;
105-
}
106150

107-
token = new TokenModel();
108-
return error;
151+
return error == null;
109152
}
110-
111-
public String GenerateAuthorizeUrl()
153+
154+
/// <summary>
155+
/// Gets a new access
156+
/// </summary>
157+
/// <param name="refreshToken"></param>
158+
/// <returns></returns>
159+
public async Task<ActionResult> RefreshToken(string refreshToken)
112160
{
113-
UriBuilder uriBuilder = new UriBuilder(ConfigurationManager.AppSettings["AuthUrl"]);
114-
var queryParams = HttpUtility.ParseQueryString(uriBuilder.Query ?? String.Empty);
115-
116-
queryParams["client_id"] = ConfigurationManager.AppSettings["ClientAppId"];
117-
queryParams["response_type"] = "Assertion";
118-
queryParams["state"] = "state";
119-
queryParams["scope"] = ConfigurationManager.AppSettings["Scope"];
120-
queryParams["redirect_uri"] = ConfigurationManager.AppSettings["CallbackUrl"];
121-
122-
uriBuilder.Query = queryParams.ToString();
161+
String error = null;
162+
if (!String.IsNullOrEmpty(refreshToken))
163+
{
164+
// Form the request to exchange an auth code for an access token and refresh token
165+
HttpRequestMessage requestMessage = new HttpRequestMessage(HttpMethod.Post, ConfigurationManager.AppSettings["TokenUrl"]);
166+
requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
123167

124-
return uriBuilder.ToString();
125-
}
168+
Dictionary<String, String> form = new Dictionary<String, String>()
169+
{
170+
{ "client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" },
171+
{ "client_assertion", ConfigurationManager.AppSettings["ClientAppSecret"] },
172+
{ "grant_type", "refresh_token" },
173+
{ "assertion", refreshToken },
174+
{ "redirect_uri", ConfigurationManager.AppSettings["CallbackUrl"] }
175+
};
176+
requestMessage.Content = new FormUrlEncodedContent(form);
177+
178+
// Make the request to exchange the auth code for an access token (and refresh token)
179+
HttpResponseMessage responseMessage = await s_httpClient.SendAsync(requestMessage);
180+
181+
if (responseMessage.IsSuccessStatusCode)
182+
{
183+
// Handle successful request
184+
String body = await responseMessage.Content.ReadAsStringAsync();
185+
ViewBag.Token = JObject.Parse(body).ToObject<TokenModel>();
186+
}
187+
else
188+
{
189+
error = responseMessage.ReasonPhrase;
190+
}
191+
}
192+
else
193+
{
194+
error = "Invalid refresh token";
195+
}
126196

127-
public string GenerateRequestPostData(string code)
128-
{
129-
return string.Format("client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer&client_assertion={0}&grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion={1}&redirect_uri={2}",
130-
HttpUtility.UrlEncode(ConfigurationManager.AppSettings["ClientAppSecret"]),
131-
HttpUtility.UrlEncode(code),
132-
ConfigurationManager.AppSettings["CallbackUrl"]
133-
);
134-
}
197+
if (!String.IsNullOrEmpty(error))
198+
{
199+
ViewBag.Error = error;
200+
}
135201

136-
public string GenerateRefreshPostData(string refreshToken)
137-
{
138-
return string.Format("client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer&client_assertion={0}&grant_type=refresh_token&assertion={1}&redirect_uri={2}",
139-
HttpUtility.UrlEncode(ConfigurationManager.AppSettings["ClientAppSecret"]),
140-
HttpUtility.UrlEncode(refreshToken),
141-
ConfigurationManager.AppSettings["CallbackUrl"]
142-
);
143-
}
202+
return View("TokenView");
203+
}
144204
}
145205
}
Lines changed: 12 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,23 @@
11
using System;
2-
using Newtonsoft.Json;
3-
2+
using System.Runtime.Serialization;
43

54
namespace OAuthSample.Models
65
{
6+
[DataContract]
77
public class TokenModel
8-
{
9-
public TokenModel()
10-
{
11-
12-
}
8+
{
9+
[DataMember(Name = "access_token")]
10+
public String AccessToken { get; set; }
1311

14-
[JsonProperty(PropertyName = "access_token")]
15-
public String accessToken { get; set; }
12+
[DataMember(Name = "token_type")]
13+
public String TokenType { get; set; }
1614

17-
[JsonProperty(PropertyName = "token_type")]
18-
public String tokenType { get; set; }
15+
[DataMember(Name = "refresh_token")]
16+
public String RefreshToken { get; set; }
1917

20-
[JsonProperty(PropertyName = "expires_in")]
21-
public String expiresIn { get; set; }
22-
23-
[JsonProperty(PropertyName = "refresh_token")]
24-
public String refreshToken { get; set; }
18+
[DataMember(Name = "expires_in")]
19+
public int ExpiresIn { get; set; }
2520

21+
public bool IsPending { get; set; }
2622
}
27-
2823
}

OAuthWebSample/OAuthWebSample/OAuthWebSample.csproj

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
<Reference Include="System.Data" />
4747
<Reference Include="System.Data.Entity" />
4848
<Reference Include="System.Drawing" />
49+
<Reference Include="System.Runtime.Serialization" />
4950
<Reference Include="System.Web.DynamicData" />
5051
<Reference Include="System.Web.Entity" />
5152
<Reference Include="System.Web.ApplicationServices" />
@@ -134,9 +135,6 @@
134135
<Content Include="Content\Site.css" />
135136
<Content Include="Scripts\bootstrap.js" />
136137
<Content Include="Scripts\bootstrap.min.js" />
137-
<None Include="Properties\PublishProfiles\OAuthSample.pubxml" />
138-
<None Include="Properties\PublishProfiles\vsoalmoauthtest.pubxml" />
139-
<None Include="Properties\PublishProfiles\vsooauthsample.pubxml" />
140138
<None Include="Scripts\jquery-1.10.2.intellisense.js" />
141139
<Content Include="Scripts\jquery-1.10.2.js" />
142140
<Content Include="Scripts\jquery-1.10.2.min.js" />
@@ -159,7 +157,6 @@
159157
<Content Include="Views\Home\Contact.cshtml" />
160158
<Content Include="Views\Home\Index.cshtml" />
161159
<Content Include="Scripts\jquery-1.10.2.min.map" />
162-
<Content Include="Views\OAuth\Index.cshtml" />
163160
<Content Include="Views\OAuth\TokenView.cshtml" />
164161
</ItemGroup>
165162
<ItemGroup />

0 commit comments

Comments
 (0)