Skip to content

Commit a48dd1a

Browse files
committed
Add support for scopes and for refreshing a token.
1 parent 60fd307 commit a48dd1a

File tree

9 files changed

+181
-65
lines changed

9 files changed

+181
-65
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
*.suo
66
*.user
77
*.sln.docstates
8+
*.sln.ide/
9+
OAuthSample/Properties/PublishProfiles/
810

911
# Build results
1012

OAuthSample/Controllers/HomeController.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ public ActionResult Index()
1212
{
1313
ViewBag.AppId = System.Configuration.ConfigurationManager.AppSettings["AppId"];
1414
ViewBag.CallbackUrl = System.Configuration.ConfigurationManager.AppSettings["CallbackUrl"];
15+
ViewBag.Scope = System.Configuration.ConfigurationManager.AppSettings["Scope"];
1516

1617
return View();
1718
}

OAuthSample/Controllers/OAuthController.cs

Lines changed: 77 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ public class OAuthController : Controller
1717
// GET: /OAuth/
1818
public ActionResult Index()
1919
{
20-
20+
2121
return View();
2222

2323
}
@@ -27,89 +27,118 @@ public ActionResult RequestToken(string code, string status)
2727
return new RedirectResult(GenerateAuthorizeUrl());
2828
}
2929

30-
public ActionResult Callback(string code, string state)
30+
public ActionResult RefreshToken(string refreshToken)
3131
{
32-
var error = String.Empty;
33-
var strResponseData = String.Empty;
34-
var strPostData = String.Empty;
32+
TokenModel token = new TokenModel();
33+
String error = null;
3534

36-
if(!String.IsNullOrEmpty(code))
35+
if (!String.IsNullOrEmpty(refreshToken))
3736
{
38-
strPostData = GeneratePostData(code);
37+
error = PerformTokenRequest(GenerateRefreshPostData(refreshToken), out token);
38+
if (String.IsNullOrEmpty(error))
39+
{
40+
ViewBag.Token = token;
41+
}
42+
}
43+
44+
ViewBag.Error = error;
3945

40-
HttpWebRequest webRequest = (HttpWebRequest)WebRequest.Create(
41-
ConfigurationManager.AppSettings["TokenUrl"]
42-
);
46+
return View("TokenView");
47+
}
4348

44-
webRequest.Method = "POST";
45-
webRequest.ContentLength = strPostData.Length;
46-
webRequest.ContentType = "application/x-www-form-urlencoded";
49+
public ActionResult Callback(string code, string state)
50+
{
51+
TokenModel token = new TokenModel();
52+
String error = null;
4753

48-
using (StreamWriter swRequestWriter = new StreamWriter(webRequest.GetRequestStream()))
54+
if (!String.IsNullOrEmpty(code))
55+
{
56+
error = PerformTokenRequest(GenerateRequestPostData(code), out token);
57+
if (String.IsNullOrEmpty(error))
4958
{
50-
swRequestWriter.Write(strPostData);
59+
ViewBag.Token = token;
5160
}
61+
}
5262

53-
try
54-
{
55-
HttpWebResponse hwrWebResponse = (HttpWebResponse)webRequest.GetResponse();
63+
ViewBag.Error = error;
5664

57-
if (hwrWebResponse.StatusCode == HttpStatusCode.OK)
58-
{
59-
using (StreamReader srResponseReader = new StreamReader(hwrWebResponse.GetResponseStream()))
60-
{
61-
strResponseData = srResponseReader.ReadToEnd();
62-
}
65+
return View("TokenView");
66+
}
6367

64-
TokenModel token = JsonConvert.DeserializeObject<TokenModel>(strResponseData);
68+
private String PerformTokenRequest(String postData, out TokenModel token)
69+
{
70+
var error = String.Empty;
71+
var strResponseData = String.Empty;
6572

66-
ViewBag.Token = token;
73+
HttpWebRequest webRequest = (HttpWebRequest)WebRequest.Create(
74+
ConfigurationManager.AppSettings["TokenUrl"]
75+
);
6776

68-
return View("TokenView");
69-
}
70-
}
71-
catch (WebException wex)
72-
{
73-
error = "<strong>Request Issue:</strong> " + wex.Message.ToString();
74-
}
75-
catch (Exception ex)
77+
webRequest.Method = "POST";
78+
webRequest.ContentLength = postData.Length;
79+
webRequest.ContentType = "application/x-www-form-urlencoded";
80+
81+
using (StreamWriter swRequestWriter = new StreamWriter(webRequest.GetRequestStream()))
82+
{
83+
swRequestWriter.Write(postData);
84+
}
85+
86+
try
87+
{
88+
HttpWebResponse hwrWebResponse = (HttpWebResponse)webRequest.GetResponse();
89+
90+
if (hwrWebResponse.StatusCode == HttpStatusCode.OK)
7691
{
77-
error = "<strong>Issue:</strong> " + ex.Message.ToString();
92+
using (StreamReader srResponseReader = new StreamReader(hwrWebResponse.GetResponseStream()))
93+
{
94+
strResponseData = srResponseReader.ReadToEnd();
95+
}
96+
97+
token = JsonConvert.DeserializeObject<TokenModel>(strResponseData);
98+
return null;
7899
}
79100
}
80-
else
101+
catch (WebException wex)
81102
{
82-
error = "<strong>Issue:</strong> Empty authorization code";
103+
error = "Request Issue: " + wex.Message;
104+
}
105+
catch (Exception ex)
106+
{
107+
error = "Issue: " + ex.Message;
83108
}
84109

85-
TokenModel emptyToken = new TokenModel();
86-
emptyToken.Error = error;
87-
88-
ViewBag.Token = emptyToken;
89-
90-
return View("TokenView");
110+
token = new TokenModel();
111+
return error;
91112
}
92113

93114
public String GenerateAuthorizeUrl()
94115
{
95-
//TODO: Add some form of state manager
96-
return String.Format("{0}?client_id={1}&response_type=Assertion&state={2}&scope=preview_api_all%20preview_msdn_licensing&redirect_uri={3}",
116+
return String.Format("{0}?client_id={1}&response_type=Assertion&state={2}&scope={3}&redirect_uri={4}",
97117
ConfigurationManager.AppSettings["AuthUrl"],
98118
ConfigurationManager.AppSettings["AppId"],
99119
"state",
120+
ConfigurationManager.AppSettings["Scope"],
100121
ConfigurationManager.AppSettings["CallbackUrl"]
101122
);
102123
}
103124

104-
public string GeneratePostData(string code)
125+
public string GenerateRequestPostData(string code)
105126
{
106127
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}",
107128
HttpUtility.UrlEncode(ConfigurationManager.AppSettings["AppSecret"]),
108129
HttpUtility.UrlEncode(code),
109130
ConfigurationManager.AppSettings["CallbackUrl"]
110131
);
111-
112132
}
113133

114-
}
134+
public string GenerateRefreshPostData(string refreshToken)
135+
{
136+
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}",
137+
HttpUtility.UrlEncode(ConfigurationManager.AppSettings["AppSecret"]),
138+
HttpUtility.UrlEncode(refreshToken),
139+
ConfigurationManager.AppSettings["CallbackUrl"]
140+
);
141+
142+
}
143+
}
115144
}

OAuthSample/Models/TokenModel.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ public TokenModel()
2323
[JsonProperty(PropertyName = "refresh_token")]
2424
public String refreshToken { get; set; }
2525

26-
public String Error { get; set; }
2726
}
2827

2928
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<!--
3+
This file is used by the publish/package process of your Web project. You can customize the behavior of this process
4+
by editing this MSBuild file. In order to learn more about this please visit http://go.microsoft.com/fwlink/?LinkID=208121.
5+
-->
6+
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
7+
<PropertyGroup>
8+
<WebPublishMethod>MSDeploy</WebPublishMethod>
9+
<LastUsedBuildConfiguration>Release</LastUsedBuildConfiguration>
10+
<LastUsedPlatform>Any CPU</LastUsedPlatform>
11+
<SiteUrlToLaunchAfterPublish>http://vsoalmoauth-tfsallin.azurewebsites.net</SiteUrlToLaunchAfterPublish>
12+
<LaunchSiteAfterPublish>True</LaunchSiteAfterPublish>
13+
<ExcludeApp_Data>False</ExcludeApp_Data>
14+
<MSDeployServiceURL>vsoalmoauth-tfsallin.scm.azurewebsites.net:443</MSDeployServiceURL>
15+
<DeployIisAppPath>vsoalmoauth-tfsallin</DeployIisAppPath>
16+
<RemoteSitePhysicalPath />
17+
<SkipExtraFilesOnServer>True</SkipExtraFilesOnServer>
18+
<MSDeployPublishMethod>WMSVC</MSDeployPublishMethod>
19+
<EnableMSDeployBackup>True</EnableMSDeployBackup>
20+
<UserName>$vsoalmoauth-tfsallin</UserName>
21+
<_SavePWD>True</_SavePWD>
22+
<_DestinationType>AzureWebSite</_DestinationType>
23+
</PropertyGroup>
24+
</Project>

OAuthSample/VSOClientOAuthSample.csproj

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,10 @@
1616
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
1717
<MvcBuildViews>false</MvcBuildViews>
1818
<UseIISExpress>true</UseIISExpress>
19-
<IISExpressSSLPort />
20-
<IISExpressAnonymousAuthentication />
21-
<IISExpressWindowsAuthentication />
22-
<IISExpressUseClassicPipelineMode />
19+
<IISExpressSSLPort>44300</IISExpressSSLPort>
20+
<IISExpressAnonymousAuthentication>enabled</IISExpressAnonymousAuthentication>
21+
<IISExpressWindowsAuthentication>disabled</IISExpressWindowsAuthentication>
22+
<IISExpressUseClassicPipelineMode>false</IISExpressUseClassicPipelineMode>
2323
</PropertyGroup>
2424
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
2525
<DebugSymbols>true</DebugSymbols>

OAuthSample/Views/Home/Index.cshtml

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,18 @@
11
@{
22
ViewBag.Title = "Visual Studio Online OAuth Client Sample";
3+
4+
var missingMsg = "Not set - update web.config";
5+
var appIdVal = !String.IsNullOrEmpty(ViewBag.AppId) ? ViewBag.AppId : missingMsg;
6+
var scopeVal = !String.IsNullOrEmpty(ViewBag.Scope) ? ViewBag.Scope : missingMsg;
7+
var callbackUrlVal = !String.IsNullOrEmpty(ViewBag.CallbackUrl) ? ViewBag.CallbackUrl : missingMsg;
38
}
49

10+
11+
512
<div class="jumbotron">
613
<h1>Visual Studio Online OAuth Client Sample</h1>
714
<p class="lead">This app shows how to authorize a user to authorize an app and then to request an access token to access Visual Studio Online on their behalf.</p>
8-
<p><a href="/oauth/requesttoken" class="btn btn-primary btn-large">Start &raquo;</a></p>
15+
<p><a href="/oauth/requesttoken" class="btn btn-primary btn-large" >Start &raquo;</a></p>
916
</div>
1017

1118
<div class="row">
@@ -15,10 +22,11 @@
1522
Before you start, make sure to:
1623
<ol>
1724
<li><a href="https://app.vssps.visualstudio.com/app/register">Register</a> a client app with Visual Studio Online</li>
18-
<li>Update the web.config of this web app and set the App ID, App Secret, and Callback URL set in the registered app. The callback URL should be https://<i>site</i>/oauth/callback
25+
<li>Update the web.config of this web app and set the App ID, Scope, App Secret, and Callback URL set in the registered app. The callback URL should be https://<i>site</i>/oauth/callback
1926
<ul>
20-
<li>App ID: <strong>@ViewBag.AppId</strong></li>
21-
<li>Callback URL: <strong>@ViewBag.CallbackUrl</strong></li>
27+
<li>App ID: <strong>@appIdVal</strong></li>
28+
<li>Scope: <strong>@scopeVal</strong></li>
29+
<li>Callback URL: <strong>@callbackUrlVal</strong></li>
2230
</ul>
2331
</li>
2432
</ol>

OAuthSample/Views/OAuth/TokenView.cshtml

Lines changed: 57 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,64 @@
22
ViewBag.Title = "Details";
33
}
44

5+
<style>
6+
7+
input[type=text], textarea {
8+
font-family: monospace;
9+
margin-bottom: 20px;
10+
max-width: 660px;
11+
}
12+
13+
#expiration {
14+
max-width: 6em;
15+
}
16+
17+
</style>
18+
519
<h2>Token details</h2>
620

7-
<p><strong>Access token: </strong>@ViewBag.Token.accessToken</p>
8-
<p><strong>Token type: </strong>@ViewBag.Token.tokenType</p>
9-
<p><strong>Expires in: </strong>@ViewBag.Token.expiresIn (seconds)</p>
10-
<p><strong>Refresh token: </strong>@ViewBag.Token.refreshToken</p>
21+
@if (!String.IsNullOrEmpty(ViewBag.Error))
22+
{
23+
<p>@ViewBag.Error</p>
24+
}
25+
else
26+
{
27+
<script>
28+
var expiration = @ViewBag.Token.expiresIn;
29+
30+
var startTime = Math.floor(Date.now() / 1000);
31+
var timer = window.setInterval(updateExpiration, 1000);
32+
function updateExpiration() {
33+
var val = Math.max(0, expiration - (Math.floor(Date.now() / 1000) - startTime));
34+
document.getElementById("expiration").value = val;
35+
36+
if (val == 0) {
37+
window.clearInterval(timer);
38+
}
39+
}
40+
41+
function selectAll(e)
42+
{
43+
e.focus();
44+
e.select();
45+
}
1146
12-
<p>@ViewBag.Token.Error</p>
47+
</script>
48+
49+
<form method="post" action="/oauth/refreshtoken">
50+
51+
<p>Access token:</p>
52+
<textarea rows="6" cols="80" onfocus="selectAll(this)" onclick="selectAll(this)" readonly="readonly" >@ViewBag.Token.accessToken</textarea>
53+
54+
<p>Refresh token:</p>
55+
<textarea rows="6" cols="80" name="refreshToken" onfocus="selectAll(this)" onclick="selectAll(this)" readonly="readonly">@ViewBag.Token.refreshToken</textarea>
56+
57+
<p>Expiration (seconds):</p>
58+
<input type="text" id="expiration" value="@ViewBag.Token.expiresIn" readonly />
59+
60+
<p><input type="submit" value="Refresh Token" /></p>
61+
62+
</form>
63+
}
1364

65+
<p><a href="/">Return to start</a></p>

OAuthSample/Web.config

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,12 @@
1111
<add key="UnobtrusiveJavaScriptEnabled" value="true" />
1212

1313
<!-- App Settings for OAuth-->
14-
<add key="AppId" value="(not set)"/>
15-
<add key="AppSecret" value="(not set)"/>
14+
<add key="AppId" value=""/>
15+
<add key="AppSecret" value=""/>
16+
<add key="Scope" value=""/>
1617
<add key="AuthUrl" value="https://app.vssps.visualstudio.com/oauth2/authorize"/>
1718
<add key="TokenUrl" value="https://app.vssps.visualstudio.com/oauth2/token"/>
18-
<add key="CallbackUrl" value="https://(not set).azurewebsites.net/oauth/callback"/>
19+
<add key="CallbackUrl" value=""/>
1920
</appSettings>
2021
<system.web>
2122
<compilation debug="true" targetFramework="4.5" />

0 commit comments

Comments
 (0)