如何在C#中获取OAuth 2.0认证令牌

111

我的设置如下:

然后我需要使用头部中的令牌进行get调用。

我可以在Postman中使其工作,但是我遇到了困难,试图弄清楚如何在C#中实现它。我一直在使用RestSharp(但也开放其他的选择)。这一切似乎都很不透明,当我认为这应该是非常简单的:这是一个控制台应用程序,所以我不需要炫耀特效。

最终,我想让我的应用程序(通过编程方式)获取一个令牌,然后将其用于我的后续调用。我希望能有人指向解释我需要的文档或示例。我找到的所有内容都是部分的,或者是针对使用不同流程的服务的。

谢谢。


你的问题得到解决了吗?如果是,请在这里分享。 - Ravi Kant Singh
是的,所选答案在下面。其他答案中也有很多好的选择。 - Matt C
10个回答

131
在Postman中,单击“生成代码”,然后在“生成代码片段”对话框中,您可以选择不同的编码语言,包括C#(RestSharp)。
此外,您只需要访问令牌URL。表单参数如下:
grant_type=client_credentials
client_id=abc    
client_secret=123

代码段:

/* using RestSharp; // https://www.nuget.org/packages/RestSharp/ */

var client = new RestClient("https://service.endpoint.com/api/oauth2/token");
var request = new RestRequest(Method.POST);
request.AddHeader("cache-control", "no-cache");
request.AddHeader("content-type", "application/x-www-form-urlencoded");
request.AddParameter("application/x-www-form-urlencoded", "grant_type=client_credentials&client_id=abc&client_secret=123", ParameterType.RequestBody);
IRestResponse response = client.Execute(request);

您可以从响应体中获取访问令牌。例如,对于Bearer令牌类型,您可以在后续的身份验证请求中添加以下标头:

request.AddHeader("authorization", "Bearer <access_token>");

4
太棒了,完美运作 - 我会归咎于自己太菜,没有注意到我可以更改“生成代码”语言。我还发现(在发布此帖子之后)我需要更改安全协议ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;(至少在我的情况下是这样),否则会遇到状态代码"0"的问题。 - Matt C
2
你在使用哪个命名空间?我不熟悉RestClient。 - Morgeth888
1
分享使用响应的access_token代码。我找不到它。我也想要Refresh_token和access_token。 - Ravi Kant Singh
在未经过用户身份验证的情况下,您绝不能允许此操作。 - c-sharp-and-swiftui-devni
我已经在Postman的编码部分使用Node.js完成了它。 - Atiq Baqi
如果您拥有scope,则将其添加到响应正文中。 - Aman Khan

116

Rest客户端的回答非常完美!(我点赞了它)

但是,万一你想要“原始”的方式

..........

我使用HttpClient让它工作了。

“抽象地”说,你正在做以下事情:

  1. 创建一个POST请求。
  2. 使用有效载荷“类型”为'x-www-form-urlencoded'的主体。(请参见FormUrlEncodedContent https://learn.microsoft.com/en-us/dotnet/api/system.net.http.formurlencodedcontent?view=net-5.0 并注意构造函数:https://learn.microsoft.com/en-us/dotnet/api/system.net.http.formurlencodedcontent.-ctor?view=net-5.0
  3. 在有效载荷“类型”:x-www-form-urlencoded中,您将放入某些值,如grant_type、client_id、client_secret等。

顺便说一句,尝试在PostMan中让它工作,然后使用下面的代码更容易“编码”。

但是,我们来看看使用HttpClient的代码。

顺带提一下,newtonsoft 的引用是为了“弥合”C#命名规范和json元素名称之间的差距。 (下面是部分代码示例)。C#属性应该是PascalCase,而json元素名称(在本例中)是“snake-case”。

[JsonProperty("access_token")]
public string AccessToken { get; set; }

如果您使用的是 .NET Core 5.0 或更高版本,则可以使用 System.Text.Json。

但现在我们来谈谈:

.......

/*
.nuget\packages\newtonsoft.json\12.0.1
.nuget\packages\system.net.http\4.3.4
*/
        
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using System.Web;
    
    
    private static async Task<Token> GetElibilityToken(HttpClient client)
    {
        string baseAddress = @"https://blah.blah.blah.com/oauth2/token";

        string grant_type = "client_credentials";
        string client_id = "myId";
        string client_secret = "shhhhhhhhhhhhhhItsSecret";

        var form = new Dictionary<string, string>
                {
                    {"grant_type", grant_type},
                    {"client_id", client_id},
                    {"client_secret", client_secret},
                };

        HttpResponseMessage tokenResponse = await client.PostAsync(baseAddress, new FormUrlEncodedContent(form));
        var jsonContent = await tokenResponse.Content.ReadAsStringAsync();
        Token tok = JsonConvert.DeserializeObject<Token>(jsonContent);
        return tok;
    }
    
    
internal class Token
{
    [JsonProperty("access_token")]
    public string AccessToken { get; set; }

    [JsonProperty("token_type")]
    public string TokenType { get; set; }

    [JsonProperty("expires_in")]
    public int ExpiresIn { get; set; }

    [JsonProperty("refresh_token")]
    public string RefreshToken { get; set; }
}       

这里是另一个工作示例(基于上面的答案)......稍微进行了一些调整。有时令牌服务会出现问题:

    private static async Task<Token> GetATokenToTestMyRestApiUsingHttpClient(HttpClient client)
    {
        /* this code has lots of commented out stuff with different permutations of tweaking the request  */

        /* this is a version of asking for token using HttpClient.  aka, an alternate to using default libraries instead of RestClient */

        OAuthValues oav = GetOAuthValues(); /* object has has simple string properties for TokenUrl, GrantType, ClientId and ClientSecret */

        var form = new Dictionary<string, string>
                {
                    { "grant_type", oav.GrantType },
                    { "client_id", oav.ClientId },
                    { "client_secret", oav.ClientSecret }
                };

        /* now tweak the http client */
        client.DefaultRequestHeaders.Clear();
        client.DefaultRequestHeaders.Add("cache-control", "no-cache");

        /* try 1 */
        ////client.DefaultRequestHeaders.Add("content-type", "application/x-www-form-urlencoded");

        /* try 2 */
        ////client.DefaultRequestHeaders            .Accept            .Add(new MediaTypeWithQualityHeaderValue("application/x-www-form-urlencoded"));//ACCEPT header

        /* try 3 */
        ////does not compile */client.Content.Headers.ContentType = new MediaTypeHeaderValue("application/x-www-form-urlencoded");

        ////application/x-www-form-urlencoded

        HttpRequestMessage req = new HttpRequestMessage(HttpMethod.Post, oav.TokenUrl);
        /////req.RequestUri = new Uri(baseAddress);
        
        req.Content = new FormUrlEncodedContent(form);

        ////string jsonPayload = "{\"grant_type\":\"" + oav.GrantType + "\",\"client_id\":\"" + oav.ClientId + "\",\"client_secret\":\"" + oav.ClientSecret + "\"}";
        ////req.Content = new StringContent(jsonPayload,                                                Encoding.UTF8,                                                "application/json");//CONTENT-TYPE header

        req.Content.Headers.ContentType = new MediaTypeHeaderValue("application/x-www-form-urlencoded");

        /* now make the request */
        ////HttpResponseMessage tokenResponse = await client.PostAsync(baseAddress, new FormUrlEncodedContent(form));
        HttpResponseMessage tokenResponse = await client.SendAsync(req);
        Console.WriteLine(string.Format("HttpResponseMessage.ReasonPhrase='{0}'", tokenResponse.ReasonPhrase));

        if (!tokenResponse.IsSuccessStatusCode)
        {
            throw new HttpRequestException("Call to get Token with HttpClient failed.");
        }

        var jsonContent = await tokenResponse.Content.ReadAsStringAsync();
        Token tok = JsonConvert.DeserializeObject<Token>(jsonContent);

        return tok;
    }

附加

额外资料!

如果您遇到

"根据验证程序,远程证书无效。"

异常......您可以添加一个处理程序来查看发生了什么(如有必要进行调整)

using System;
using System.Collections.Generic;
using System.Text;
using Newtonsoft.Json;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using System.Web;
using System.Net;

namespace MyNamespace
{
    public class MyTokenRetrieverWithExtraStuff
    {
        public static async Task<Token> GetElibilityToken()
        {
            using (HttpClientHandler httpClientHandler = new HttpClientHandler())
            {
                httpClientHandler.ServerCertificateCustomValidationCallback = CertificateValidationCallBack;
                using (HttpClient client = new HttpClient(httpClientHandler))
                {
                    return await GetElibilityToken(client);
                }
            }
        }

        private static async Task<Token> GetElibilityToken(HttpClient client)
        {
            // throws certificate error if your cert is wired to localhost // 
            //string baseAddress = @"https://127.0.0.1/someapp/oauth2/token";

            //string baseAddress = @"https://localhost/someapp/oauth2/token";

        string baseAddress = @"https://blah.blah.blah.com/oauth2/token";

        string grant_type = "client_credentials";
        string client_id = "myId";
        string client_secret = "shhhhhhhhhhhhhhItsSecret";

        var form = new Dictionary<string, string>
                {
                    {"grant_type", grant_type},
                    {"client_id", client_id},
                    {"client_secret", client_secret},
                };

            HttpResponseMessage tokenResponse = await client.PostAsync(baseAddress, new FormUrlEncodedContent(form));
            var jsonContent = await tokenResponse.Content.ReadAsStringAsync();
            Token tok = JsonConvert.DeserializeObject<Token>(jsonContent);
            return tok;
        }

        private static bool CertificateValidationCallBack(
        object sender,
        System.Security.Cryptography.X509Certificates.X509Certificate certificate,
        System.Security.Cryptography.X509Certificates.X509Chain chain,
        System.Net.Security.SslPolicyErrors sslPolicyErrors)
        {
            // If the certificate is a valid, signed certificate, return true.
            if (sslPolicyErrors == System.Net.Security.SslPolicyErrors.None)
            {
                return true;
            }

            // If there are errors in the certificate chain, look at each error to determine the cause.
            if ((sslPolicyErrors & System.Net.Security.SslPolicyErrors.RemoteCertificateChainErrors) != 0)
            {
                if (chain != null && chain.ChainStatus != null)
                {
                    foreach (System.Security.Cryptography.X509Certificates.X509ChainStatus status in chain.ChainStatus)
                    {
                        if ((certificate.Subject == certificate.Issuer) &&
                           (status.Status == System.Security.Cryptography.X509Certificates.X509ChainStatusFlags.UntrustedRoot))
                        {
                            // Self-signed certificates with an untrusted root are valid. 
                            continue;
                        }
                        else
                        {
                            if (status.Status != System.Security.Cryptography.X509Certificates.X509ChainStatusFlags.NoError)
                            {
                                // If there are any other errors in the certificate chain, the certificate is invalid,
                                // so the method returns false.
                                return false;
                            }
                        }
                    }
                }

                // When processing reaches this line, the only errors in the certificate chain are 
                // untrusted root errors for self-signed certificates. These certificates are valid
                // for default Exchange server installations, so return true.
                return true;
            }


            /* overcome localhost and 127.0.0.1 issue */
            if ((sslPolicyErrors & System.Net.Security.SslPolicyErrors.RemoteCertificateNameMismatch) != 0)
            {
                if (certificate.Subject.Contains("localhost"))
                {
                    HttpRequestMessage castSender = sender as HttpRequestMessage;
                    if (null != castSender)
                    {
                        if (castSender.RequestUri.Host.Contains("127.0.0.1"))
                        {
                            return true;
                        }
                    }
                }
            }

            return false;

        }


        public class Token
        {
            [JsonProperty("access_token")]
            public string AccessToken { get; set; }

            [JsonProperty("token_type")]
            public string TokenType { get; set; }

            [JsonProperty("expires_in")]
            public int ExpiresIn { get; set; }

            [JsonProperty("refresh_token")]
            public string RefreshToken { get; set; }
        }

    }
}

........................

我最近发现(2020年1月)一篇关于这方面的文章。 我会在这里添加一个链接....有时候让两个不同的人展示/解释它可以帮助正在尝试学习它的人。

http://luisquintanilla.me/2017/12/25/client-credentials-authentication-csharp/


4
谢谢!这有助于尽可能减少 NuGet 包的使用。 - Jordan_Walters
一个使用证书的排列怎么样? - bkwdesign
你能提供一下你所指的oauth2授权类型的链接吗?https://alexbilbie.com/guide-to-oauth-2-grants/ 说到底,你只是在处理OAuth风格的HTTP请求(具有特定的头部、有效载荷中的特定项等等)。 - granadaCoder
从STS(安全令牌服务)读取令牌和生成令牌(作为STS)是完全不同的事情。 - granadaCoder
1
谢谢!答案末尾的链接已更改。现在是https://www.luisquintanilla.me/posts/client-credentials-authentication-csharp.html - Rolf Staflin
显示剩余3条评论

16

这是一个完整的例子。右键单击解决方案以管理NuGet包并获取Newtonsoft和RestSharp:

using Newtonsoft.Json.Linq;
using RestSharp;
using System;


namespace TestAPI
{
    class Program
    {
        static void Main(string[] args)
        {
            string id = "xxx";
            string secret = "xxx";

            var client = new RestClient("https://xxx.xxx.com/services/api/oauth2/token");
            var request = new RestRequest(Method.POST);
            request.AddHeader("cache-control", "no-cache");
            request.AddHeader("content-type", "application/x-www-form-urlencoded");
            request.AddParameter("application/x-www-form-urlencoded", "grant_type=client_credentials&scope=all&client_id=" + id + "&client_secret=" + secret, ParameterType.RequestBody);
            RestResponse response = client.Execute(request);

            dynamic resp = JObject.Parse(response.Content);
            string token = resp.access_token;            

            client = new RestClient("https://xxx.xxx.com/services/api/x/users/v1/employees");
            request = new RestRequest(Method.GET);
            request.AddHeader("authorization", "Bearer " + token);
            request.AddHeader("cache-control", "no-cache");
            response = client.Execute(request);
        }        
    }
}

使用上述代码出现错误: 在 Newtonsoft.Json.dll 中发生了类型为 'Newtonsoft.Json.JsonReaderException' 的异常,但未在用户代码中处理。附加信息:从 JsonReader 读取 JObject 时出错。路径“”,行 0,位置 0。 - Ravi Kant Singh
@RaviKantSingh 你需要检查一下你的 response。我猜想你的请求失败了,导致你得到了错误的响应,这是 JObject 无法理解的。 - Long Luong

12

我使用了ADAL.NET/Microsoft Identity Platform 来实现这个功能。使用它的好处是我们可以获得一个很好的包装器来获取AccessToken的代码,我们还可以获得像Token Cache之类的额外功能。以下是文档中的内容:

为什么使用ADAL.NET?

ADAL.NET V3 (Active Directory Authentication Library for .NET)使.NET应用程序开发人员能够获取token以调用受保护的Web API。这些Web API可以是Microsoft Graph或第三方Web API。

这是代码片段:

    // Import Nuget package: Microsoft.Identity.Client
    public class AuthenticationService
    {
         private readonly List<string> _scopes;
         private readonly IConfidentialClientApplication _app;

        public AuthenticationService(AuthenticationConfiguration authentication)
        {

             _app = ConfidentialClientApplicationBuilder
                         .Create(authentication.ClientId)
                         .WithClientSecret(authentication.ClientSecret)
                         .WithAuthority(authentication.Authority)
                         .Build();

           _scopes = new List<string> {$"{authentication.Audience}/.default"};
       }

       public async Task<string> GetAccessToken()
       {
           var authenticationResult = await _app.AcquireTokenForClient(_scopes) 
                                                .ExecuteAsync();
           return authenticationResult.AccessToken;
       }
   }

1
能否请下投票者说明一下为什么要点踩? - Ankit Vijay
2
它难道不仅限于微软令牌吗? - Yet Another Code Maker
文档说明应该也可以与第三方合作。 - Ankit Vijay
我不读那个。据我所知,唯一的令牌提供商是Azure AD。 - Yet Another Code Maker
1
@Konrad ADAL已被MSAL取代。请阅读文档:https://learn.microsoft.com/en-us/azure/active-directory/develop/msal-overview - Sven
显示剩余2条评论

4
您可以使用以下代码获取Bearer令牌。
private string GetBearerToken()
{
    var client = new RestClient("https://service.endpoint.com");
    client.Authenticator = new HttpBasicAuthenticator("abc", "123");
    var request = new RestRequest("api/oauth2/token", Method.POST);
    request.AddHeader("content-type", "application/json");
    request.AddParameter("application/json", "{ \"grant_type\":\"client_credentials\" }", 
    ParameterType.RequestBody);
    var responseJson = _client.Execute(request).Content;
    var token = JsonConvert.DeserializeObject<Dictionary<string, object>>(responseJson)["access_token"].ToString();
    if(token.Length == 0)
    {
        throw new AuthenticationException("API authentication failed.");
    }
    return token;
}

2
我的客户正在使用“grant_type=Authorization_code”工作流程。
我有以下设置:
认证URL(恰好是“https://login.microsoftonline.com/…”),如果这有助于理解。
访问令牌URL:“https://service.endpoint.com/api/oauth2/token” 客户端ID:“xyz” 客户端密钥:“123dfsdf”
然后,我需要使用标头中的令牌进行GET调用。我尝试了所有上述代码示例,但无论如何都会跳转到Microsoft - 登录到您的帐户页面。
"\r\n\r\n<!-- Copyright (C) Microsoft Corporation. All rights reserved. -->\r\n<!DOCTYPE html>\r\n<html dir=\"ltr\" class=\"\" lang=\"en\">\r\n<head>\r\n    <title>Sign in to your account</title>\r\n "

我可以在Postman上执行,发现控制台中有两个调用。
  1. 获取授权码的GET调用
  2. 在调用访问令牌时附加上述授权码的POST调用。
我尝试在POSTMAN中单独执行上述GET调用,这时会提示我输入Microsoftonline登录页面的凭据,但输入凭据后我会收到Salesforce错误。
如果有人能提供Authorization_code grand_type工作流程的示例代码或示例,那将非常有帮助...

2

这个例子通过 HttpWebRequest 获取令牌。

        HttpWebRequest request = (HttpWebRequest)WebRequest.Create(pathapi);
        request.Method = "POST";
        string postData = "grant_type=password";
        ASCIIEncoding encoding = new ASCIIEncoding();
        byte[] byte1 = encoding.GetBytes(postData);

        request.ContentType = "application/x-www-form-urlencoded";

        request.ContentLength = byte1.Length;
        Stream newStream = request.GetRequestStream();
        newStream.Write(byte1, 0, byte1.Length);

        HttpWebResponse response = request.GetResponse() as HttpWebResponse;            
        using (Stream responseStream = response.GetResponseStream())
        {
            StreamReader reader = new StreamReader(responseStream, Encoding.UTF8);
            getreaderjson = reader.ReadToEnd();
        }

1

https://github.com/IdentityModel/IdentityModel添加了扩展到HttpClient,以使用不同的流程获取令牌,并且文档也非常好。这非常方便,因为您不必考虑如何自己实现它。我不知道是否存在任何官方的MS实现。


1

我尝试了使用C#获取OAuth 2.0认证令牌的方法

namespace ConsoleApp2
{

    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine(GetToken());
            Console.Read();
        }

        /// <summary>
        /// Get access token from api
        /// </summary>
        /// <returns></returns>
        private static string GetToken()
        {
            string wClientId = "#######";
            string wClientSecretKey = "*********************";
            string wAccessToken;

//--------------------------- Approch-1 to get token using HttpClient -------------------------------------------------------------------------------------
            HttpResponseMessage responseMessage;
            using (HttpClient client = new HttpClient())
            {
                HttpRequestMessage tokenRequest = new HttpRequestMessage(HttpMethod.Post, "https://localhost:1001/oauth/token");
                HttpContent httpContent = new FormUrlEncodedContent(
                        new[]
                        {
                                        new KeyValuePair<string, string>("grant_type", "client_credentials"),
                        });
                tokenRequest.Content = httpContent;
                tokenRequest.Headers.Add("Authorization", "Basic " + Convert.ToBase64String(Encoding.Default.GetBytes(wClientId + ":" + wClientSecretKey)));
                responseMessage =  client.SendAsync(tokenRequest).Result;
            }
            string ResponseJSON=   responseMessage.Content.ReadAsStringAsync().Result;


//--------------------------- Approch-2 to get token using HttpWebRequest and deserialize json object into ResponseModel class -------------------------------------------------------------------------------------


            byte[] byte1 = Encoding.ASCII.GetBytes("grant_type=client_credentials");

            HttpWebRequest oRequest = WebRequest.Create("https://localhost:1001/oauth/token") as HttpWebRequest;
            oRequest.Accept = "application/json";
            oRequest.Method = "POST";
            oRequest.ContentType = "application/x-www-form-urlencoded";
            oRequest.ContentLength = byte1.Length;
            oRequest.KeepAlive = false;
            oRequest.Headers.Add("Authorization", "Basic " + Convert.ToBase64String(Encoding.Default.GetBytes(wClientId + ":" + wClientSecretKey)));
            Stream newStream = oRequest.GetRequestStream();
            newStream.Write(byte1, 0, byte1.Length);

            WebResponse oResponse = oRequest.GetResponse();

            using (var reader = new StreamReader(oResponse.GetResponseStream(), Encoding.UTF8))
            {
                var oJsonReponse = reader.ReadToEnd();
                ResponseModel oModel = JsonConvert.DeserializeObject<ResponseModel>(oJsonReponse);
                wAccessToken = oModel.access_token;
            }

            return wAccessToken;
        }
    }
  
  //----------------------------------------------------------------------------------------------------------------------------------------------------
  //---------------------------------- Response Class---------------------------------------------------------------------------------------
  //----------------------------------------------------------------------------------------------------------------------------------------------------

    /// <summary>
    /// De-serialize Web response Object into model class to read  
    /// </summary>
    public class ResponseModel
    {
        public string scope { get; set; }
        public string token_type { get; set; }
        public string expires_in { get; set; }
        public string refresh_token { get; set; }
        public string access_token { get; set; }
    }
}

1

明确:

服务器端生成令牌示例

private string GenerateToken(string userName)
{
    var someClaims = new Claim[]{
        new Claim(JwtRegisteredClaimNames.UniqueName, userName),
        new Claim(JwtRegisteredClaimNames.Email, GetEmail(userName)),
        new Claim(JwtRegisteredClaimNames.NameId,Guid.NewGuid().ToString())
    };

    SecurityKey securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_settings.Tokenizer.Key));
    var token = new JwtSecurityToken(
        issuer: _settings.Tokenizer.Issuer,
        audience: _settings.Tokenizer.Audience,
        claims: someClaims,
        expires: DateTime.Now.AddHours(_settings.Tokenizer.ExpiryHours),
        signingCredentials: new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256)
    );

    return new JwtSecurityTokenHandler().WriteToken(token);
}

(注意:Tokenizer是我的辅助类,其中包含Issuer Audience等)

明确:

客户端获取用于身份验证的令牌

    public async Task<string> GetToken()
    {
        string token = "";
        var siteSettings = DependencyResolver.Current.GetService<SiteSettings>();

        var client = new HttpClient();
        client.BaseAddress = new Uri(siteSettings.PopularSearchRequest.StaticApiUrl);
        client.DefaultRequestHeaders.Accept.Clear();
        //client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

        StatisticUserModel user = new StatisticUserModel()
        {
            Password = siteSettings.PopularSearchRequest.Password,
            Username = siteSettings.PopularSearchRequest.Username
        };

        string jsonUser = JsonConvert.SerializeObject(user, Formatting.Indented);
        var stringContent = new StringContent(jsonUser, Encoding.UTF8, "application/json");
        var response = await client.PostAsync(siteSettings.PopularSearchRequest.StaticApiUrl + "/api/token/new", stringContent);
        token = await response.Content.ReadAsStringAsync();

        return token;
    }

你可以使用这个 token 进行授权(在之后的请求中)。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接