|
1 | 1 | using System;
|
2 | 2 | using System.Configuration;
|
| 3 | +using System.Collections.Generic; |
3 | 4 | using System.IO;
|
4 | 5 | using System.Net;
|
5 | 6 | using System.Web;
|
6 | 7 | using System.Web.Mvc;
|
7 | 8 | using Newtonsoft.Json;
|
8 | 9 | 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; |
9 | 15 |
|
10 | 16 | namespace OAuthSample.Controllers
|
11 | 17 | {
|
12 | 18 | public class OAuthController : Controller
|
13 | 19 | {
|
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() |
17 | 30 | {
|
| 31 | + Guid state = Guid.NewGuid(); |
18 | 32 |
|
19 |
| - return View(); |
| 33 | + s_authorizationRequests[state] = new TokenModel() { IsPending = true }; |
| 34 | + |
| 35 | + return new RedirectResult(GetAuthorizationUrl(state.ToString())); |
20 | 36 | }
|
21 | 37 |
|
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) |
23 | 44 | {
|
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(); |
25 | 57 | }
|
26 | 58 |
|
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) |
28 | 66 | {
|
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)) |
33 | 69 | {
|
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>() |
36 | 75 | {
|
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); |
40 | 83 |
|
41 |
| - ViewBag.Error = error; |
| 84 | + HttpResponseMessage responseMessage = await s_httpClient.SendAsync(requestMessage); |
42 | 85 |
|
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(); |
50 | 89 |
|
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 |
55 | 96 | {
|
56 |
| - ViewBag.Token = token; |
| 97 | + error = responseMessage.ReasonPhrase; |
57 | 98 | }
|
58 | 99 | }
|
59 | 100 |
|
60 |
| - ViewBag.Error = error; |
| 101 | + if (!String.IsNullOrEmpty(error)) |
| 102 | + { |
| 103 | + ViewBag.Error = error; |
| 104 | + } |
| 105 | + |
| 106 | + ViewBag.ProfileUrl = ConfigurationManager.AppSettings["ProfileUrl"]; |
61 | 107 |
|
62 | 108 | return View("TokenView");
|
63 | 109 | }
|
64 | 110 |
|
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) |
66 | 119 | {
|
67 |
| - var error = String.Empty; |
68 |
| - var strResponseData = String.Empty; |
| 120 | + error = null; |
69 | 121 |
|
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)) |
79 | 123 | {
|
80 |
| - swRequestWriter.Write(postData); |
| 124 | + error = "Invalid auth code"; |
81 | 125 | }
|
82 |
| - |
83 |
| - try |
| 126 | + else |
84 | 127 | {
|
85 |
| - HttpWebResponse hwrWebResponse = (HttpWebResponse)webRequest.GetResponse(); |
86 |
| - |
87 |
| - if (hwrWebResponse.StatusCode == HttpStatusCode.OK) |
| 128 | + Guid authorizationRequestKey; |
| 129 | + if (!Guid.TryParse(state, out authorizationRequestKey)) |
88 | 130 | {
|
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)) |
90 | 137 | {
|
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 |
92 | 147 | }
|
93 |
| - |
94 |
| - token = JsonConvert.DeserializeObject<TokenModel>(strResponseData); |
95 |
| - return null; |
96 | 148 | }
|
97 | 149 | }
|
98 |
| - catch (WebException wex) |
99 |
| - { |
100 |
| - error = "Request Issue: " + wex.Message; |
101 |
| - } |
102 |
| - catch (Exception ex) |
103 |
| - { |
104 |
| - error = "Issue: " + ex.Message; |
105 |
| - } |
106 | 150 |
|
107 |
| - token = new TokenModel(); |
108 |
| - return error; |
| 151 | + return error == null; |
109 | 152 | }
|
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) |
112 | 160 | {
|
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")); |
123 | 167 |
|
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 | + } |
126 | 196 |
|
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 | + } |
135 | 201 |
|
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 | + } |
144 | 204 | }
|
145 | 205 | }
|
0 commit comments