Skip to content

Commit c8c923a

Browse files
zhengchunkevinchalet
authored andcommitted
Add a QQ provider
1 parent c8ae943 commit c8c923a

9 files changed

+486
-0
lines changed

AspNet.Security.OAuth.Providers.sln

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AspNet.Security.OAuth.Disco
103103
EndProject
104104
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AspNet.Security.OAuth.Patreon", "src\AspNet.Security.OAuth.Patreon\AspNet.Security.OAuth.Patreon.csproj", "{ED8A220C-45FE-45C6-9B8F-BB009ACE972E}"
105105
EndProject
106+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AspNet.Security.OAuth.QQ", "src\AspNet.Security.OAuth.QQ\AspNet.Security.OAuth.QQ.csproj", "{7C2E82CE-F6EC-41A8-AA22-3466505F95D8}"
107+
EndProject
106108
Global
107109
GlobalSection(SolutionConfigurationPlatforms) = preSolution
108110
Debug|Any CPU = Debug|Any CPU
@@ -297,6 +299,10 @@ Global
297299
{ED8A220C-45FE-45C6-9B8F-BB009ACE972E}.Debug|Any CPU.Build.0 = Debug|Any CPU
298300
{ED8A220C-45FE-45C6-9B8F-BB009ACE972E}.Release|Any CPU.ActiveCfg = Release|Any CPU
299301
{ED8A220C-45FE-45C6-9B8F-BB009ACE972E}.Release|Any CPU.Build.0 = Release|Any CPU
302+
{7C2E82CE-F6EC-41A8-AA22-3466505F95D8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
303+
{7C2E82CE-F6EC-41A8-AA22-3466505F95D8}.Debug|Any CPU.Build.0 = Debug|Any CPU
304+
{7C2E82CE-F6EC-41A8-AA22-3466505F95D8}.Release|Any CPU.ActiveCfg = Release|Any CPU
305+
{7C2E82CE-F6EC-41A8-AA22-3466505F95D8}.Release|Any CPU.Build.0 = Release|Any CPU
300306
EndGlobalSection
301307
GlobalSection(SolutionProperties) = preSolution
302308
HideSolutionNode = FALSE
@@ -349,5 +355,6 @@ Global
349355
{C66A6EDB-D29A-49EE-841E-75F239DE5A04} = {C1352FD3-AE8B-43EE-B45B-F6E0B3FBAC6D}
350356
{86614CB9-0768-40BF-8C27-699E3990B733} = {C1352FD3-AE8B-43EE-B45B-F6E0B3FBAC6D}
351357
{ED8A220C-45FE-45C6-9B8F-BB009ACE972E} = {C1352FD3-AE8B-43EE-B45B-F6E0B3FBAC6D}
358+
{7C2E82CE-F6EC-41A8-AA22-3466505F95D8} = {C1352FD3-AE8B-43EE-B45B-F6E0B3FBAC6D}
352359
EndGlobalSection
353360
EndGlobal

samples/Mvc.Client/Mvc.Client.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
<ProjectReference Include="..\..\src\AspNet.Security.OAuth.Onshape\AspNet.Security.OAuth.Onshape.csproj" />
3535
<ProjectReference Include="..\..\src\AspNet.Security.OAuth.Patreon\AspNet.Security.OAuth.Patreon.csproj" />
3636
<ProjectReference Include="..\..\src\AspNet.Security.OAuth.Paypal\AspNet.Security.OAuth.Paypal.csproj" />
37+
<ProjectReference Include="..\..\src\AspNet.Security.OAuth.QQ\AspNet.Security.OAuth.QQ.csproj" />
3738
<ProjectReference Include="..\..\src\AspNet.Security.OAuth.Reddit\AspNet.Security.OAuth.Reddit.csproj" />
3839
<ProjectReference Include="..\..\src\AspNet.Security.OAuth.Salesforce\AspNet.Security.OAuth.Salesforce.csproj" />
3940
<ProjectReference Include="..\..\src\AspNet.Security.OAuth.Slack\AspNet.Security.OAuth.Slack.csproj" />
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<Import Project="..\..\build\packages.props" />
4+
5+
<PropertyGroup>
6+
<TargetFrameworks>net451;netstandard1.3</TargetFrameworks>
7+
</PropertyGroup>
8+
9+
<PropertyGroup>
10+
<Description>ASP.NET Core security middleware enabling QQ authentication.</Description>
11+
<Authors>zhengchun</Authors>
12+
<PackageTags>aspnetcore;authentication;oauth;qq;security;tencent</PackageTags>
13+
</PropertyGroup>
14+
15+
<ItemGroup>
16+
<Compile Include="..\..\shared\AspNet.Security.OAuth.Extensions\*.cs" />
17+
</ItemGroup>
18+
19+
<ItemGroup>
20+
<PackageReference Include="JetBrains.Annotations" Version="$(JetBrainsVersion)" PrivateAssets="All" />
21+
<PackageReference Include="Microsoft.AspNetCore.Authentication.OAuth" Version="$(AspNetCoreVersion)" />
22+
</ItemGroup>
23+
24+
</Project>
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/*
2+
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
3+
* See https://github.com/aspnet-contrib/AspNet.Security.OAuth.Providers
4+
* for more information concerning the license and the contributors participating to this project.
5+
*/
6+
7+
using Microsoft.AspNetCore.Builder;
8+
9+
namespace AspNet.Security.OAuth.QQ
10+
{
11+
/// <summary>
12+
/// Default values for QQ authentication.
13+
/// </summary>
14+
public static class QQAuthenticationDefaults
15+
{
16+
/// <summary>
17+
/// Default value for <see cref="AuthenticationOptions.AuthenticationScheme"/>.
18+
/// </summary>
19+
public const string AuthenticationScheme = "QQ";
20+
21+
/// <summary>
22+
/// Default value for <see cref="RemoteAuthenticationOptions.CallbackPath"/>.
23+
/// </summary>
24+
public const string CallbackPath = "/signin-qq";
25+
26+
/// <summary>
27+
/// Default value for <see cref="AuthenticationOptions.ClaimsIssuer"/>.
28+
/// </summary>
29+
public const string Issuer = "QQ";
30+
31+
/// <summary>
32+
/// Default value for <see cref="OAuthOptions.AuthorizationEndpoint"/>.
33+
/// </summary>
34+
public const string AuthorizationEndpoint = "https://graph.qq.com/oauth2.0/authorize";
35+
36+
/// <summary>
37+
/// Default value for <see cref="OAuthOptions.TokenEndpoint"/>.
38+
/// </summary>
39+
public const string TokenEndpoint = "https://graph.qq.com/oauth2.0/token";
40+
41+
/// <summary>
42+
/// Default value for <see cref="QQAuthenticationOptions.UserIdentificationEndpoint"/>.
43+
/// </summary>
44+
public const string UserIdentificationEndpoint = "https://graph.qq.com/oauth2.0/me";
45+
46+
/// <summary>
47+
/// Default value for <see cref="OAuthOptions.UserInformationEndpoint"/>.
48+
/// </summary>
49+
public const string UserInformationEndpoint = "https://graph.qq.com/user/get_user_info";
50+
}
51+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/*
2+
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
3+
* See https://github.com/aspnet-contrib/AspNet.Security.OAuth.Providers
4+
* for more information concerning the license and the contributors participating to this project.
5+
*/
6+
7+
using System;
8+
using AspNet.Security.OAuth.QQ;
9+
using JetBrains.Annotations;
10+
using Microsoft.Extensions.Options;
11+
12+
namespace Microsoft.AspNetCore.Builder
13+
{
14+
/// <summary>
15+
/// Extension methods to add QQ authentication capabilities to an HTTP application pipeline.
16+
/// </summary>
17+
public static class QQAuthenticationExtensions
18+
{
19+
/// <summary>
20+
/// Adds the <see cref="QQAuthenticationMiddleware"/> middleware to the specified
21+
/// <see cref="IApplicationBuilder"/>, which enables Weibo authentication capabilities.
22+
/// </summary>
23+
/// <param name="app">The <see cref="IApplicationBuilder"/> to add the middleware to.</param>
24+
/// <param name="options">A <see cref="QQAuthenticationOptions"/> that specifies options for the middleware.</param>
25+
/// <returns>A reference to this instance after the operation has completed.</returns>
26+
public static IApplicationBuilder UseQQAuthentication(
27+
[NotNull] this IApplicationBuilder app,
28+
[NotNull] QQAuthenticationOptions options)
29+
{
30+
if (app == null)
31+
{
32+
throw new ArgumentNullException(nameof(app));
33+
}
34+
35+
if (options == null)
36+
{
37+
throw new ArgumentNullException(nameof(options));
38+
}
39+
40+
return app.UseMiddleware<QQAuthenticationMiddleware>(Options.Create(options));
41+
}
42+
43+
/// <summary>
44+
/// Adds the <see cref="QQAuthenticationMiddleware"/> middleware to the specified
45+
/// <see cref="IApplicationBuilder"/>, which enables Weibo authentication capabilities.
46+
/// </summary>
47+
/// <param name="app">The <see cref="IApplicationBuilder"/> to add the middleware to.</param>
48+
/// <param name="configuration">An action delegate to configure the provided <see cref="QQAuthenticationOptions"/>.</param>
49+
/// <returns>A reference to this instance after the operation has completed.</returns>
50+
public static IApplicationBuilder UseQQAuthentication(
51+
[NotNull] this IApplicationBuilder app,
52+
[NotNull] Action<QQAuthenticationOptions> configuration)
53+
{
54+
if (app == null)
55+
{
56+
throw new ArgumentNullException(nameof(app));
57+
}
58+
59+
if (configuration == null)
60+
{
61+
throw new ArgumentNullException(nameof(configuration));
62+
}
63+
64+
var options = new QQAuthenticationOptions();
65+
configuration(options);
66+
67+
return app.UseMiddleware<QQAuthenticationMiddleware>(Options.Create(options));
68+
}
69+
}
70+
}
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
/*
2+
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
3+
* See https://github.com/aspnet-contrib/AspNet.Security.OAuth.Providers
4+
* for more information concerning the license and the contributors participating to this project.
5+
*/
6+
7+
using System;
8+
using System.Collections.Generic;
9+
using System.Linq;
10+
using System.Net.Http;
11+
using System.Security.Claims;
12+
using System.Threading.Tasks;
13+
using AspNet.Security.OAuth.Extensions;
14+
using JetBrains.Annotations;
15+
using Microsoft.AspNetCore.Authentication;
16+
using Microsoft.AspNetCore.Authentication.OAuth;
17+
using Microsoft.AspNetCore.Http.Authentication;
18+
using Microsoft.AspNetCore.WebUtilities;
19+
using Microsoft.Extensions.Logging;
20+
using Newtonsoft.Json.Linq;
21+
22+
namespace AspNet.Security.OAuth.QQ
23+
{
24+
public class QQAuthenticationHandler : OAuthHandler<QQAuthenticationOptions>
25+
{
26+
public QQAuthenticationHandler([NotNull] HttpClient client)
27+
: base(client)
28+
{
29+
}
30+
31+
protected override async Task<AuthenticationTicket> CreateTicketAsync([NotNull] ClaimsIdentity identity,
32+
[NotNull] AuthenticationProperties properties, [NotNull] OAuthTokenResponse tokens)
33+
{
34+
var identifier = await GetUserIdentifierAsync(tokens);
35+
if (string.IsNullOrEmpty(identifier))
36+
{
37+
throw new HttpRequestException("An error occurred while retrieving the user identifier.");
38+
}
39+
40+
var address = QueryHelpers.AddQueryString(Options.UserInformationEndpoint, new Dictionary<string, string>
41+
{
42+
["oauth_consumer_key"] = Options.ClientId,
43+
["access_token"] = tokens.AccessToken,
44+
["openid"] = identifier,
45+
});
46+
47+
var response = await Backchannel.GetAsync(address);
48+
if (!response.IsSuccessStatusCode)
49+
{
50+
Logger.LogError("An error occurred while retrieving the user profile: the remote server " +
51+
"returned a {Status} response with the following payload: {Headers} {Body}.",
52+
/* Status: */ response.StatusCode,
53+
/* Headers: */ response.Headers.ToString(),
54+
/* Body: */ await response.Content.ReadAsStringAsync());
55+
56+
throw new HttpRequestException("An error occurred while retrieving user information.");
57+
}
58+
59+
var payload = JObject.Parse(await response.Content.ReadAsStringAsync());
60+
61+
var status = payload.Value<int>("ret");
62+
if (status != 0)
63+
{
64+
Logger.LogError("An error occurred while retrieving the user profile: the remote server " +
65+
"returned a {Status} response with the following message: {Message}.",
66+
/* Status: */ status,
67+
/* Message: */ payload.Value<string>("msg"));
68+
69+
throw new HttpRequestException("An error occurred while retrieving user information.");
70+
}
71+
72+
identity.AddOptionalClaim("urn:qq:picture", QQAuthenticationHelper.GetPicture(payload), Options.ClaimsIssuer)
73+
.AddOptionalClaim("urn:qq:picture_medium", QQAuthenticationHelper.GetPictureMedium(payload), Options.ClaimsIssuer)
74+
.AddOptionalClaim("urn:qq:picture_full", QQAuthenticationHelper.GetPictureFull(payload), Options.ClaimsIssuer)
75+
.AddOptionalClaim("urn:qq:avatar", QQAuthenticationHelper.GetAvatar(payload), Options.ClaimsIssuer)
76+
.AddOptionalClaim("urn:qq:avatar_full", QQAuthenticationHelper.GetAvatarFull(payload), Options.ClaimsIssuer);
77+
78+
var principal = new ClaimsPrincipal(identity);
79+
var ticket = new AuthenticationTicket(principal, properties, Options.AuthenticationScheme);
80+
81+
var context = new OAuthCreatingTicketContext(ticket, Context, Options, Backchannel, tokens, payload);
82+
await Options.Events.CreatingTicket(context);
83+
84+
return context.Ticket;
85+
}
86+
87+
protected override async Task<OAuthTokenResponse> ExchangeCodeAsync([NotNull] string code, [NotNull] string redirectUri)
88+
{
89+
var address = QueryHelpers.AddQueryString(Options.TokenEndpoint, new Dictionary<string, string>()
90+
{
91+
["client_id"] = Options.ClientId,
92+
["client_secret"] = Options.ClientSecret,
93+
["redirect_uri"] = redirectUri,
94+
["code"] = code,
95+
["grant_type"] = "authorization_code",
96+
});
97+
98+
var request = new HttpRequestMessage(HttpMethod.Get, address);
99+
100+
var response = await Backchannel.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, Context.RequestAborted);
101+
if (!response.IsSuccessStatusCode)
102+
{
103+
Logger.LogError("An error occurred while retrieving an access token: the remote server " +
104+
"returned a {Status} response with the following payload: {Headers} {Body}.",
105+
/* Status: */ response.StatusCode,
106+
/* Headers: */ response.Headers.ToString(),
107+
/* Body: */ await response.Content.ReadAsStringAsync());
108+
109+
return OAuthTokenResponse.Failed(new Exception("An error occurred while retrieving an access token."));
110+
}
111+
112+
var payload = JObject.FromObject(QueryHelpers.ParseQuery(await response.Content.ReadAsStringAsync())
113+
.ToDictionary(pair => pair.Key, k => k.Value.ToString()));
114+
115+
return OAuthTokenResponse.Success(payload);
116+
}
117+
118+
private async Task<string> GetUserIdentifierAsync(OAuthTokenResponse tokens)
119+
{
120+
var address = QueryHelpers.AddQueryString(Options.UserIdentificationEndpoint, "access_token", tokens.AccessToken);
121+
var request = new HttpRequestMessage(HttpMethod.Get, address);
122+
123+
var response = await Backchannel.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, Context.RequestAborted);
124+
if (!response.IsSuccessStatusCode)
125+
{
126+
Logger.LogError("An error occurred while retrieving the user identifier: the remote server " +
127+
"returned a {Status} response with the following payload: {Headers} {Body}.",
128+
/* Status: */ response.StatusCode,
129+
/* Headers: */ response.Headers.ToString(),
130+
/* Body: */ await response.Content.ReadAsStringAsync());
131+
132+
throw new HttpRequestException("An error occurred while retrieving the user identifier.");
133+
}
134+
135+
var body = await response.Content.ReadAsStringAsync();
136+
137+
var index = body.IndexOf("{");
138+
if (index > 0)
139+
{
140+
body = body.Substring(index, body.LastIndexOf("}") - index + 1);
141+
}
142+
143+
var payload = JObject.Parse(body);
144+
145+
return payload.Value<string>("openid");
146+
}
147+
148+
protected override string FormatScope() => string.Join(",", Options.Scope);
149+
}
150+
}

0 commit comments

Comments
 (0)