From 3c738c4f5fb7c7d0a553030192707929deff27b4 Mon Sep 17 00:00:00 2001 From: Justin Maier Date: Thu, 16 Apr 2015 18:36:11 -0700 Subject: [PATCH 1/2] Added Visual Studio Online oAuth support https://www.visualstudio.com/integrate/get-started/auth/oauth --- .../Owin.Security.Providers.csproj | 9 + .../VisualStudio/Constants.cs | 11 + .../IVisualStudioAuthenticationProvider.cs | 23 ++ .../VisualStudioAuthenticatedContext.cs | 82 +++++++ .../VisualStudioAuthenticationProvider.cs | 47 ++++ .../VisualStudioReturnEndpointContext.cs | 23 ++ .../VisualStudioAuthenticationExtensions.cs | 21 ++ .../VisualStudioAuthenticationHandler.cs | 209 ++++++++++++++++++ .../VisualStudioAuthenticationMiddleware.cs | 80 +++++++ .../VisualStudioAuthenticationOptions.cs | 141 ++++++++++++ .../App_Start/Startup.Auth.cs | 3 + README.md | 1 + 12 files changed, 650 insertions(+) create mode 100644 Owin.Security.Providers/VisualStudio/Constants.cs create mode 100644 Owin.Security.Providers/VisualStudio/Provider/IVisualStudioAuthenticationProvider.cs create mode 100644 Owin.Security.Providers/VisualStudio/Provider/VisualStudioAuthenticatedContext.cs create mode 100644 Owin.Security.Providers/VisualStudio/Provider/VisualStudioAuthenticationProvider.cs create mode 100644 Owin.Security.Providers/VisualStudio/Provider/VisualStudioReturnEndpointContext.cs create mode 100644 Owin.Security.Providers/VisualStudio/VisualStudioAuthenticationExtensions.cs create mode 100644 Owin.Security.Providers/VisualStudio/VisualStudioAuthenticationHandler.cs create mode 100644 Owin.Security.Providers/VisualStudio/VisualStudioAuthenticationMiddleware.cs create mode 100644 Owin.Security.Providers/VisualStudio/VisualStudioAuthenticationOptions.cs diff --git a/Owin.Security.Providers/Owin.Security.Providers.csproj b/Owin.Security.Providers/Owin.Security.Providers.csproj index 7808169..e952a9f 100644 --- a/Owin.Security.Providers/Owin.Security.Providers.csproj +++ b/Owin.Security.Providers/Owin.Security.Providers.csproj @@ -273,6 +273,15 @@ + + + + + + + + + diff --git a/Owin.Security.Providers/VisualStudio/Constants.cs b/Owin.Security.Providers/VisualStudio/Constants.cs new file mode 100644 index 0000000..9d32b17 --- /dev/null +++ b/Owin.Security.Providers/VisualStudio/Constants.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Owin.Security.Providers.VisualStudio { + internal static class Constants { + public const string DefaultAuthenticationType = "Visual Studio Online"; + } +} diff --git a/Owin.Security.Providers/VisualStudio/Provider/IVisualStudioAuthenticationProvider.cs b/Owin.Security.Providers/VisualStudio/Provider/IVisualStudioAuthenticationProvider.cs new file mode 100644 index 0000000..2ea50b3 --- /dev/null +++ b/Owin.Security.Providers/VisualStudio/Provider/IVisualStudioAuthenticationProvider.cs @@ -0,0 +1,23 @@ +using System.Threading.Tasks; + +namespace Owin.Security.Providers.VisualStudio { + + /// + /// Specifies callback methods which the invokes to enable developer control over the authentication process. /> + /// + public interface IVisualStudioAuthenticationProvider { + /// + /// Invoked whenever Visual Studio Online succesfully authenticates a user + /// + /// Contains information about the login session as well as the user . + /// A representing the completed operation. + Task Authenticated(VisualStudioAuthenticatedContext context); + + /// + /// Invoked prior to the being saved in a local cookie and the browser being redirected to the originally requested URL. + /// + /// + /// A representing the completed operation. + Task ReturnEndpoint(VisualStudioReturnEndpointContext context); + } +} diff --git a/Owin.Security.Providers/VisualStudio/Provider/VisualStudioAuthenticatedContext.cs b/Owin.Security.Providers/VisualStudio/Provider/VisualStudioAuthenticatedContext.cs new file mode 100644 index 0000000..54b6137 --- /dev/null +++ b/Owin.Security.Providers/VisualStudio/Provider/VisualStudioAuthenticatedContext.cs @@ -0,0 +1,82 @@ +using System; +using System.Globalization; +using System.Security.Claims; +using Microsoft.Owin; +using Microsoft.Owin.Security; +using Microsoft.Owin.Security.Provider; +using Newtonsoft.Json.Linq; + +namespace Owin.Security.Providers.VisualStudio { + + /// + /// Contains information about the login session as well as the user . + /// + public class VisualStudioAuthenticatedContext : BaseContext{ + + /// + /// Initializes a + /// + /// The OWIN environment + /// The JSON-serialized user + /// Visual Studio Online Access token + public VisualStudioAuthenticatedContext(IOwinContext context, JObject user, string accessToken) + : base(context) + { + AccessToken = accessToken; + User = user; + + Id = TryGetValue(user, "id"); + Name = TryGetValue(user, "displayName"); + Email = TryGetValue(user, "emailAddress"); + Alias = TryGetValue(user, "publicAlias"); + } + + /// + /// Gets the JSON-serialized user + /// + /// + /// Contains the Visual Studio user obtained from the endpoint https://app.vssps.visualstudio.com/_apis/profile/profiles/me + /// + public JObject User { get; private set; } + + /// + /// Gets the Visual Studio Online OAuth access token + /// + public string AccessToken { get; private set; } + + /// + /// Get the user's id + /// + public string Id { get; private set; } + + /// + /// Get the user's displayName + /// + public string Name { get; private set; } + + /// + /// Get the user's email + /// + public string Email { get; private set; } + + /// + /// Get the user's publicAlias + /// + public string Alias { get; private set; } + + /// + /// Gets the representing the user + /// + public ClaimsIdentity Identity { get; set; } + + /// + /// Gets or sets a property bag for common authentication properties + /// + public AuthenticationProperties Properties { get; set; } + + private static string TryGetValue(JObject user, string propertyName) { + JToken value; + return user.TryGetValue(propertyName, out value) ? value.ToString() : null; + } + } +} diff --git a/Owin.Security.Providers/VisualStudio/Provider/VisualStudioAuthenticationProvider.cs b/Owin.Security.Providers/VisualStudio/Provider/VisualStudioAuthenticationProvider.cs new file mode 100644 index 0000000..43af752 --- /dev/null +++ b/Owin.Security.Providers/VisualStudio/Provider/VisualStudioAuthenticationProvider.cs @@ -0,0 +1,47 @@ +using System; +using System.Threading.Tasks; + +namespace Owin.Security.Providers.VisualStudio { + + /// + /// Default implementation. + /// + public class VisualStudioAuthenticationProvider : IVisualStudioAuthenticationProvider { + /// + /// Initializes a + /// + public VisualStudioAuthenticationProvider() + { + OnAuthenticated = context => Task.FromResult(null); + OnReturnEndpoint = context => Task.FromResult(null); + } + + /// + /// Gets or sets the function that is invoked when the Authenticated method is invoked. + /// + public Func OnAuthenticated { get; set; } + + /// + /// Gets or sets the function that is invoked when the ReturnEndpoint method is invoked. + /// + public Func OnReturnEndpoint { get; set; } + + /// + /// Invoked whenever Visual Studio Online succesfully authenticates a user + /// + /// Contains information about the login session as well as the user . + /// A representing the completed operation. + public virtual Task Authenticated(VisualStudioAuthenticatedContext context) { + return OnAuthenticated(context); + } + + /// + /// Invoked prior to the being saved in a local cookie and the browser being redirected to the originally requested URL. + /// + /// + /// A representing the completed operation. + public virtual Task ReturnEndpoint(VisualStudioReturnEndpointContext context) { + return OnReturnEndpoint(context); + } + } +} diff --git a/Owin.Security.Providers/VisualStudio/Provider/VisualStudioReturnEndpointContext.cs b/Owin.Security.Providers/VisualStudio/Provider/VisualStudioReturnEndpointContext.cs new file mode 100644 index 0000000..b18d13a --- /dev/null +++ b/Owin.Security.Providers/VisualStudio/Provider/VisualStudioReturnEndpointContext.cs @@ -0,0 +1,23 @@ +using Microsoft.Owin; +using Microsoft.Owin.Security; +using Microsoft.Owin.Security.Provider; + +namespace Owin.Security.Providers.VisualStudio { + + /// + /// Provides context information to middleware providers. + /// + public class VisualStudioReturnEndpointContext : ReturnEndpointContext { + /// + /// + /// + /// OWIN environment + /// The authentication ticket + public VisualStudioReturnEndpointContext( + IOwinContext context, + AuthenticationTicket ticket) + : base(context, ticket) + { + } + } +} diff --git a/Owin.Security.Providers/VisualStudio/VisualStudioAuthenticationExtensions.cs b/Owin.Security.Providers/VisualStudio/VisualStudioAuthenticationExtensions.cs new file mode 100644 index 0000000..9b79a17 --- /dev/null +++ b/Owin.Security.Providers/VisualStudio/VisualStudioAuthenticationExtensions.cs @@ -0,0 +1,21 @@ +using System; + +namespace Owin.Security.Providers.VisualStudio { + public static class VisualStudioAuthenticationExtensions { + public static IAppBuilder UseVisualStudioAuthentication(this IAppBuilder app, VisualStudioAuthenticationOptions options) { + if (app == null) throw new ArgumentNullException("app"); + if (options == null) throw new ArgumentNullException("options"); + + app.Use(typeof(VisualStudioAuthenticationMiddleware), app, options); + + return app; + } + + public static IAppBuilder UseVisualStudioAuthentication(this IAppBuilder app, string appId, string appSecret) { + return app.UseVisualStudioAuthentication(new VisualStudioAuthenticationOptions { + AppId = appId, + AppSecret = appSecret + }); + } + } +} diff --git a/Owin.Security.Providers/VisualStudio/VisualStudioAuthenticationHandler.cs b/Owin.Security.Providers/VisualStudio/VisualStudioAuthenticationHandler.cs new file mode 100644 index 0000000..295ce38 --- /dev/null +++ b/Owin.Security.Providers/VisualStudio/VisualStudioAuthenticationHandler.cs @@ -0,0 +1,209 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.Owin; +using Microsoft.Owin.Infrastructure; +using Microsoft.Owin.Logging; +using Microsoft.Owin.Security; +using Microsoft.Owin.Security.Infrastructure; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Owin.Security.Providers.VisualStudio { + public class VisualStudioAuthenticationHandler : AuthenticationHandler { + private const string XmlSchemaString = "http://www.w3.org/2001/XMLSchema#string"; + + private readonly ILogger logger; + private readonly HttpClient httpClient; + + public VisualStudioAuthenticationHandler(HttpClient httpClient, ILogger logger) { + this.httpClient = httpClient; + this.logger = logger; + } + + protected override async Task AuthenticateCoreAsync() { + AuthenticationProperties properties = null; + + try { + string code = null; + string state = null; + + IReadableStringCollection query = Request.Query; + IList values = query.GetValues("code"); + if (values != null && values.Count == 1) { + code = values[0]; + } + values = query.GetValues("state"); + if (values != null && values.Count == 1) { + state = values[0]; + } + + properties = Options.StateDataFormat.Unprotect(state); + if (properties == null) { + return null; + } + + // OAuth2 10.12 CSRF + if (!ValidateCorrelationId(properties, logger)) { + return new AuthenticationTicket(null, properties); + } + + string requestPrefix = "https://" + Request.Host; // Schema must be HTTPS + string redirectUri = requestPrefix + Request.PathBase + Options.CallbackPath; + + // Build up the body for the token request + var body = new List>(); + body.Add(new KeyValuePair("client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer")); + body.Add(new KeyValuePair("client_assertion", Options.AppSecret)); + body.Add(new KeyValuePair("grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer")); + body.Add(new KeyValuePair("assertion", code)); + body.Add(new KeyValuePair("redirect_uri", redirectUri)); + + // Request the token + var requestMessage = new HttpRequestMessage(HttpMethod.Post, Options.Endpoints.TokenEndpoint); + requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + requestMessage.Content = new FormUrlEncodedContent(body); + HttpResponseMessage tokenResponse = await httpClient.SendAsync(requestMessage); + tokenResponse.EnsureSuccessStatusCode(); + string text = await tokenResponse.Content.ReadAsStringAsync(); + + // Deserializes the token response + dynamic response = JsonConvert.DeserializeObject(text); + string accessToken = (string)response.access_token; + + // Get the Visual Studio Online user + HttpRequestMessage userRequest = new HttpRequestMessage(HttpMethod.Get, Options.Endpoints.UserInfoEndpoint); + userRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); + userRequest.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + HttpResponseMessage userResponse = await httpClient.SendAsync(userRequest, Request.CallCancelled); + userResponse.EnsureSuccessStatusCode(); + text = await userResponse.Content.ReadAsStringAsync(); + JObject user = JObject.Parse(text); + + var context = new VisualStudioAuthenticatedContext(Context, user, accessToken); + context.Identity = new ClaimsIdentity( + Options.AuthenticationType, + ClaimsIdentity.DefaultNameClaimType, + ClaimsIdentity.DefaultRoleClaimType); + if (!string.IsNullOrEmpty(context.Id)) { + context.Identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, context.Id, XmlSchemaString, Options.AuthenticationType)); + } + if (!string.IsNullOrEmpty(context.Name)) { + context.Identity.AddClaim(new Claim(ClaimsIdentity.DefaultNameClaimType, context.Name, XmlSchemaString, Options.AuthenticationType)); + } + if (!string.IsNullOrEmpty(context.Email)) { + context.Identity.AddClaim(new Claim(ClaimTypes.Email, context.Email, XmlSchemaString, Options.AuthenticationType)); + } + if (!string.IsNullOrEmpty(context.Alias)) { + context.Identity.AddClaim(new Claim("urn:vso:alias", context.Alias, XmlSchemaString, Options.AuthenticationType)); + } + context.Properties = properties; + + await Options.Provider.Authenticated(context); + + return new AuthenticationTicket(context.Identity, context.Properties); + } catch (Exception ex) { + logger.WriteError(ex.Message); + } + return new AuthenticationTicket(null, properties); + } + + protected override Task ApplyResponseChallengeAsync() { + if (Response.StatusCode != 401) { + return Task.FromResult(null); + } + + AuthenticationResponseChallenge challenge = Helper.LookupChallenge(Options.AuthenticationType, Options.AuthenticationMode); + + if (challenge != null) { + string baseUri = + "https" + //Schema must be HTTPS + Uri.SchemeDelimiter + + Request.Host + + Request.PathBase; + + string currentUri = + baseUri + + Request.Path + + Request.QueryString; + + string redirectUri = + baseUri + + Options.CallbackPath; + + AuthenticationProperties properties = challenge.Properties; + if (string.IsNullOrEmpty(properties.RedirectUri)) { + properties.RedirectUri = currentUri; + } + + // OAuth2 10.12 CSRF + GenerateCorrelationId(properties); + + // space separated + string scope = string.Join(" ", Options.Scope); + + string state = Options.StateDataFormat.Protect(properties); + + string authorizationEndpoint = + Options.Endpoints.AuthorizationEndpoint + + "?client_id=" + Uri.EscapeDataString(Options.AppId) + + "&response_type=Assertion" + + "&state=" + Uri.EscapeDataString(state) + + "&scope=" + Uri.EscapeDataString(scope) + + "&redirect_uri=" + Uri.EscapeDataString(redirectUri); + + Response.Redirect(authorizationEndpoint); + } + + return Task.FromResult(null); + } + + public override async Task InvokeAsync() { + return await InvokeReplyPathAsync(); + } + + private async Task InvokeReplyPathAsync() { + if (Options.CallbackPath.HasValue && Options.CallbackPath == Request.Path) { + // TODO: error responses + + AuthenticationTicket ticket = await AuthenticateAsync(); + if (ticket == null) { + logger.WriteWarning("Invalid return state, unable to redirect."); + Response.StatusCode = 500; + return true; + } + + var context = new VisualStudioReturnEndpointContext(Context, ticket); + context.SignInAsAuthenticationType = Options.SignInAsAuthenticationType; + context.RedirectUri = ticket.Properties.RedirectUri; + + await Options.Provider.ReturnEndpoint(context); + + if (context.SignInAsAuthenticationType != null && + context.Identity != null) { + ClaimsIdentity grantIdentity = context.Identity; + if (!string.Equals(grantIdentity.AuthenticationType, context.SignInAsAuthenticationType, StringComparison.Ordinal)) { + grantIdentity = new ClaimsIdentity(grantIdentity.Claims, context.SignInAsAuthenticationType, grantIdentity.NameClaimType, grantIdentity.RoleClaimType); + } + Context.Authentication.SignIn(context.Properties, grantIdentity); + } + + if (!context.IsRequestCompleted && context.RedirectUri != null) { + string redirectUri = context.RedirectUri; + if (context.Identity == null) { + // add a redirect hint that sign-in failed in some way + redirectUri = WebUtilities.AddQueryString(redirectUri, "error", "access_denied"); + } + Response.Redirect(redirectUri); + context.RequestCompleted(); + } + + return context.IsRequestCompleted; + } + return false; + } + } +} diff --git a/Owin.Security.Providers/VisualStudio/VisualStudioAuthenticationMiddleware.cs b/Owin.Security.Providers/VisualStudio/VisualStudioAuthenticationMiddleware.cs new file mode 100644 index 0000000..b94fde1 --- /dev/null +++ b/Owin.Security.Providers/VisualStudio/VisualStudioAuthenticationMiddleware.cs @@ -0,0 +1,80 @@ +using System; +using System.Globalization; +using System.Net.Http; +using Microsoft.Owin; +using Microsoft.Owin.Logging; +using Microsoft.Owin.Security; +using Microsoft.Owin.Security.DataHandler; +using Microsoft.Owin.Security.DataProtection; +using Microsoft.Owin.Security.Infrastructure; +using Owin.Security.Providers.Properties; + +namespace Owin.Security.Providers.VisualStudio { + public class VisualStudioAuthenticationMiddleware : AuthenticationMiddleware { + private readonly HttpClient httpClient; + private readonly ILogger logger; + + public VisualStudioAuthenticationMiddleware(OwinMiddleware next, IAppBuilder app, VisualStudioAuthenticationOptions options) + : base(next, options) + { + if (String.IsNullOrWhiteSpace(Options.AppId)) + throw new ArgumentException(String.Format(CultureInfo.CurrentCulture, + Resources.Exception_OptionMustBeProvided, "ClientId")); + if (String.IsNullOrWhiteSpace(Options.AppSecret)) + throw new ArgumentException(String.Format(CultureInfo.CurrentCulture, + Resources.Exception_OptionMustBeProvided, "ClientSecret")); + + logger = app.CreateLogger(); + + if (Options.Provider == null) + Options.Provider = new VisualStudioAuthenticationProvider(); + + if (Options.StateDataFormat == null) + { + IDataProtector dataProtector = app.CreateDataProtector( + typeof (VisualStudioAuthenticationMiddleware).FullName, + Options.AuthenticationType, "v1"); + Options.StateDataFormat = new PropertiesDataFormat(dataProtector); + } + + if (String.IsNullOrEmpty(Options.SignInAsAuthenticationType)) + Options.SignInAsAuthenticationType = app.GetDefaultSignInAsAuthenticationType(); + + httpClient = new HttpClient(ResolveHttpMessageHandler(Options)) + { + Timeout = Options.BackchannelTimeout, + MaxResponseContentBufferSize = 1024*1024*10, + }; + httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("Microsoft Owin VisualStudio middleware"); + httpClient.DefaultRequestHeaders.ExpectContinue = false; + } + + /// + /// Provides the object for processing + /// authentication-related requests. + /// + /// + /// An configured with the + /// supplied to the constructor. + /// + protected override AuthenticationHandler CreateHandler() { + return new VisualStudioAuthenticationHandler(httpClient, logger); + } + + private HttpMessageHandler ResolveHttpMessageHandler(VisualStudioAuthenticationOptions options) { + HttpMessageHandler handler = options.BackchannelHttpHandler ?? new WebRequestHandler(); + + // If they provided a validator, apply it or fail. + if (options.BackchannelCertificateValidator != null) { + // Set the cert validate callback + var webRequestHandler = handler as WebRequestHandler; + if (webRequestHandler == null) { + throw new InvalidOperationException(Resources.Exception_ValidatorHandlerMismatch); + } + webRequestHandler.ServerCertificateValidationCallback = options.BackchannelCertificateValidator.Validate; + } + + return handler; + } + } +} diff --git a/Owin.Security.Providers/VisualStudio/VisualStudioAuthenticationOptions.cs b/Owin.Security.Providers/VisualStudio/VisualStudioAuthenticationOptions.cs new file mode 100644 index 0000000..c7390c8 --- /dev/null +++ b/Owin.Security.Providers/VisualStudio/VisualStudioAuthenticationOptions.cs @@ -0,0 +1,141 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using Microsoft.Owin; +using Microsoft.Owin.Security; + +namespace Owin.Security.Providers.VisualStudio { + public class VisualStudioAuthenticationOptions : AuthenticationOptions { + public class VisualStudioAuthenticationEndpoints { + /// + /// Endpoint which is used to redirect users to request Visual Studio Online access + /// + /// + /// Defaults to https://app.vssps.visualstudio.com/oauth2/authorize + /// + public string AuthorizationEndpoint { get; set; } + + /// + /// Endpoint which is used to exchange code for access token + /// + /// + /// Defaults to https://app.vssps.visualstudio.com/oauth2/token + /// + public string TokenEndpoint { get; set; } + + /// + /// Endpoint which is used to obtain user information after authentication + /// + /// + /// Defaults to https://app.vssps.visualstudio.com/_apis/profile/profiles/me + /// + public string UserInfoEndpoint { get; set; } + } + + private const string AuthorizationEndPoint = "https://app.vssps.visualstudio.com/oauth2/authorize"; + private const string TokenEndpoint = "https://app.vssps.visualstudio.com/oauth2/token"; + private const string UserInfoEndpoint = "https://app.vssps.visualstudio.com/_apis/profile/profiles/me?api-version=1.0"; + + /// + /// Gets or sets the a pinned certificate validator to use to validate the endpoints used + /// in back channel communications belong to Visual Studio Online. + /// + /// + /// The pinned certificate validator. + /// + /// + /// If this property is null then the default certificate checks are performed, + /// validating the subject name and if the signing chain is a trusted party. + /// + public ICertificateValidator BackchannelCertificateValidator { get; set; } + + /// + /// The HttpMessageHandler used to communicate with Visual Studio Online. + /// This cannot be set at the same time as BackchannelCertificateValidator unless the value + /// can be downcast to a WebRequestHandler. + /// + public HttpMessageHandler BackchannelHttpHandler { get; set; } + + /// + /// Gets or sets timeout value in milliseconds for back channel communications with Visual Studio Online. + /// + /// + /// The back channel timeout in milliseconds. + /// + public TimeSpan BackchannelTimeout { get; set; } + + /// + /// The request path within the application's base path where the user-agent will be returned. + /// The middleware will process this request when it arrives. + /// Default value is "/signin-visualstudio". + /// + public PathString CallbackPath { get; set; } + + /// + /// Get or sets the text that the user can display on a sign in user interface. + /// + public string Caption { + get { return Description.Caption; } + set { Description.Caption = value; } + } + + /// + /// Gets or sets the Visual Studio Online supplied Application Id + /// + public string AppId { get; set; } + + /// + /// Gets or sets the Visual Studio Online supplied Application Secret + /// + public string AppSecret { get; set; } + + /// + /// Gets the sets of OAuth endpoints used to authenticate against Visual Studio. + /// authentication. + /// + public VisualStudioAuthenticationEndpoints Endpoints { get; set; } + + /// + /// Gets or sets the used in the authentication events + /// + public IVisualStudioAuthenticationProvider Provider { get; set; } + + /// + /// A list of permissions to request. + /// + public IList Scope { get; private set; } + + /// + /// Gets or sets the name of another authentication middleware which will be responsible for actually issuing a user + /// . + /// + public string SignInAsAuthenticationType { get; set; } + + /// + /// Gets or sets the type used to secure data handled by the middleware. + /// + public ISecureDataFormat StateDataFormat { get; set; } + + /// + /// Initializes a new + /// + public VisualStudioAuthenticationOptions() + : base(Constants.DefaultAuthenticationType) + { + Caption = Constants.DefaultAuthenticationType; + CallbackPath = new PathString("/signin-visualstudio"); + AuthenticationMode = AuthenticationMode.Passive; + Scope = new List + { + "vso.profile" + }; + BackchannelTimeout = TimeSpan.FromSeconds(60); + Endpoints = new VisualStudioAuthenticationEndpoints + { + AuthorizationEndpoint = AuthorizationEndPoint, + TokenEndpoint = TokenEndpoint, + UserInfoEndpoint = UserInfoEndpoint + }; + } + } +} diff --git a/OwinOAuthProvidersDemo/App_Start/Startup.Auth.cs b/OwinOAuthProvidersDemo/App_Start/Startup.Auth.cs index 21e96f0..4bc02b7 100644 --- a/OwinOAuthProvidersDemo/App_Start/Startup.Auth.cs +++ b/OwinOAuthProvidersDemo/App_Start/Startup.Auth.cs @@ -221,6 +221,9 @@ namespace OwinOAuthProvidersDemo //app.UseWargamingAccountAuthentication("", WargamingAuthenticationOptions.Region.NorthAmerica); //app.UseFlickrAuthentication("", ""); + //app.UseVisualStudioAuthentication( + // appId: "", + // appSecret: ""); } } } \ No newline at end of file diff --git a/README.md b/README.md index e8ec94e..877ed54 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ Provides a set of extra authentication providers for OWIN ([Project Katana](http - HealthGraph - Battle.net - Asana + - Visual Studio Online - OpenID - Generic OpenID 2.0 provider - Steam From 94eacc949f37bfedce66904af049d47afe20dda0 Mon Sep 17 00:00:00 2001 From: Justin Maier Date: Thu, 16 Apr 2015 20:12:49 -0700 Subject: [PATCH 2/2] Add VSO Refresh Token --- .../Provider/VisualStudioAuthenticatedContext.cs | 14 +++++++++++++- .../VisualStudioAuthenticationHandler.cs | 4 +++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/Owin.Security.Providers/VisualStudio/Provider/VisualStudioAuthenticatedContext.cs b/Owin.Security.Providers/VisualStudio/Provider/VisualStudioAuthenticatedContext.cs index 54b6137..41ba043 100644 --- a/Owin.Security.Providers/VisualStudio/Provider/VisualStudioAuthenticatedContext.cs +++ b/Owin.Security.Providers/VisualStudio/Provider/VisualStudioAuthenticatedContext.cs @@ -19,11 +19,13 @@ namespace Owin.Security.Providers.VisualStudio { /// The OWIN environment /// The JSON-serialized user /// Visual Studio Online Access token - public VisualStudioAuthenticatedContext(IOwinContext context, JObject user, string accessToken) + public VisualStudioAuthenticatedContext(IOwinContext context, JObject user, string accessToken, int expiresIn, string refreshToken) : base(context) { AccessToken = accessToken; User = user; + RefreshToken = refreshToken; + ExpiresIn = TimeSpan.FromSeconds(expiresIn); Id = TryGetValue(user, "id"); Name = TryGetValue(user, "displayName"); @@ -44,6 +46,16 @@ namespace Owin.Security.Providers.VisualStudio { /// public string AccessToken { get; private set; } + /// + /// Gets the Google OAuth refresh token. This is only available when the RequestOfflineAccess property of is set to true + /// + public string RefreshToken { get; private set; } + + /// + /// Gets the Google+ access token expiration time + /// + public TimeSpan? ExpiresIn { get; set; } + /// /// Get the user's id /// diff --git a/Owin.Security.Providers/VisualStudio/VisualStudioAuthenticationHandler.cs b/Owin.Security.Providers/VisualStudio/VisualStudioAuthenticationHandler.cs index 295ce38..94f44d3 100644 --- a/Owin.Security.Providers/VisualStudio/VisualStudioAuthenticationHandler.cs +++ b/Owin.Security.Providers/VisualStudio/VisualStudioAuthenticationHandler.cs @@ -73,6 +73,8 @@ namespace Owin.Security.Providers.VisualStudio { // Deserializes the token response dynamic response = JsonConvert.DeserializeObject(text); string accessToken = (string)response.access_token; + string refreshToken = (string)response.refresh_token; + int expiresIn = (int)response.expires_in; // Get the Visual Studio Online user HttpRequestMessage userRequest = new HttpRequestMessage(HttpMethod.Get, Options.Endpoints.UserInfoEndpoint); @@ -83,7 +85,7 @@ namespace Owin.Security.Providers.VisualStudio { text = await userResponse.Content.ReadAsStringAsync(); JObject user = JObject.Parse(text); - var context = new VisualStudioAuthenticatedContext(Context, user, accessToken); + var context = new VisualStudioAuthenticatedContext(Context, user, accessToken, expiresIn, refreshToken); context.Identity = new ClaimsIdentity( Options.AuthenticationType, ClaimsIdentity.DefaultNameClaimType,