mirror of
https://github.com/bitwarden/server.git
synced 2025-04-16 18:48:16 -05:00
duo api class
This commit is contained in:
parent
73a2fa27ee
commit
1a856fb2ab
278
src/Core/Utilities/DuoApi.cs
Normal file
278
src/Core/Utilities/DuoApi.cs
Normal file
@ -0,0 +1,278 @@
|
|||||||
|
/*
|
||||||
|
Original source modified from https://github.com/duosecurity/duo_api_csharp
|
||||||
|
|
||||||
|
=============================================================================
|
||||||
|
=============================================================================
|
||||||
|
|
||||||
|
Copyright (c) 2018 Duo Security
|
||||||
|
All rights reserved
|
||||||
|
*/
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Net;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using System.Text;
|
||||||
|
using System.Web;
|
||||||
|
using System.Globalization;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace Bit.Core.Utilities.Duo
|
||||||
|
{
|
||||||
|
public class DuoApi
|
||||||
|
{
|
||||||
|
private const string UrlScheme = "https";
|
||||||
|
private const string UserAgent = "Bitwarden_DuoAPICSharp/1.0 (.NET Core)";
|
||||||
|
|
||||||
|
private readonly string _host;
|
||||||
|
private readonly string _ikey;
|
||||||
|
private readonly string _skey;
|
||||||
|
|
||||||
|
public DuoApi(string ikey, string skey, string host)
|
||||||
|
{
|
||||||
|
_ikey = ikey;
|
||||||
|
_skey = skey;
|
||||||
|
_host = host;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string CanonicalizeParams(Dictionary<string, string> parameters)
|
||||||
|
{
|
||||||
|
var ret = new List<string>();
|
||||||
|
foreach(var pair in parameters)
|
||||||
|
{
|
||||||
|
var p = string.Format("{0}={1}", HttpUtility.UrlEncode(pair.Key), HttpUtility.UrlEncode(pair.Value));
|
||||||
|
// Signatures require upper-case hex digits.
|
||||||
|
p = Regex.Replace(p, "(%[0-9A-Fa-f][0-9A-Fa-f])", c => c.Value.ToUpperInvariant());
|
||||||
|
// Escape only the expected characters.
|
||||||
|
p = Regex.Replace(p, "([!'()*])", c => "%" + Convert.ToByte(c.Value[0]).ToString("X"));
|
||||||
|
p = p.Replace("%7E", "~");
|
||||||
|
// UrlEncode converts space (" ") to "+". The
|
||||||
|
// signature algorithm requires "%20" instead. Actual
|
||||||
|
// + has already been replaced with %2B.
|
||||||
|
p = p.Replace("+", "%20");
|
||||||
|
ret.Add(p);
|
||||||
|
}
|
||||||
|
|
||||||
|
ret.Sort(StringComparer.Ordinal);
|
||||||
|
return string.Join("&", ret.ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected string CanonicalizeRequest(string method, string path, string canonParams, string date)
|
||||||
|
{
|
||||||
|
string[] lines = {
|
||||||
|
date,
|
||||||
|
method.ToUpperInvariant(),
|
||||||
|
_host.ToLower(),
|
||||||
|
path,
|
||||||
|
canonParams,
|
||||||
|
};
|
||||||
|
return string.Join("\n", lines);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Sign(string method, string path, string canonParams, string date)
|
||||||
|
{
|
||||||
|
var canon = CanonicalizeRequest(method, path, canonParams, date);
|
||||||
|
var sig = HmacSign(canon);
|
||||||
|
var auth = string.Concat(_ikey, ':', sig);
|
||||||
|
return string.Concat("Basic ", Encode64(auth));
|
||||||
|
}
|
||||||
|
|
||||||
|
public string ApiCall(string method, string path, Dictionary<string, string> parameters)
|
||||||
|
{
|
||||||
|
return ApiCall(method, path, parameters, 0, out var statusCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <param name="timeout">The request timeout, in milliseconds.
|
||||||
|
/// Specify 0 to use the system-default timeout. Use caution if
|
||||||
|
/// you choose to specify a custom timeout - some API
|
||||||
|
/// calls (particularly in the Auth APIs) will not
|
||||||
|
/// return a response until an out-of-band authentication process
|
||||||
|
/// has completed. In some cases, this may take as much as a
|
||||||
|
/// small number of minutes.</param>
|
||||||
|
public string ApiCall(string method, string path, Dictionary<string, string> parameters, int timeout,
|
||||||
|
out HttpStatusCode statusCode)
|
||||||
|
{
|
||||||
|
var canonParams = CanonicalizeParams(parameters);
|
||||||
|
var query = string.Empty;
|
||||||
|
if(!method.Equals("POST") && !method.Equals("PUT"))
|
||||||
|
{
|
||||||
|
if(parameters.Count > 0)
|
||||||
|
{
|
||||||
|
query = "?" + canonParams;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var url = string.Format("{0}://{1}{2}{3}", UrlScheme, _host, path, query);
|
||||||
|
|
||||||
|
var dateString = RFC822UtcNow();
|
||||||
|
var auth = Sign(method, path, canonParams, dateString);
|
||||||
|
|
||||||
|
var request = (HttpWebRequest)WebRequest.Create(url);
|
||||||
|
request.Method = method;
|
||||||
|
request.Accept = "application/json";
|
||||||
|
request.Headers.Add("Authorization", auth);
|
||||||
|
request.Headers.Add("X-Duo-Date", dateString);
|
||||||
|
request.UserAgent = UserAgent;
|
||||||
|
|
||||||
|
if(method.Equals("POST") || method.Equals("PUT"))
|
||||||
|
{
|
||||||
|
var data = Encoding.UTF8.GetBytes(canonParams);
|
||||||
|
request.ContentType = "application/x-www-form-urlencoded";
|
||||||
|
request.ContentLength = data.Length;
|
||||||
|
using(var requestStream = request.GetRequestStream())
|
||||||
|
{
|
||||||
|
requestStream.Write(data, 0, data.Length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(timeout > 0)
|
||||||
|
{
|
||||||
|
request.Timeout = timeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do the request and process the result.
|
||||||
|
HttpWebResponse response;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
response = (HttpWebResponse)request.GetResponse();
|
||||||
|
}
|
||||||
|
catch(WebException ex)
|
||||||
|
{
|
||||||
|
response = (HttpWebResponse)ex.Response;
|
||||||
|
if(response == null)
|
||||||
|
{
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
using(var reader = new StreamReader(response.GetResponseStream()))
|
||||||
|
{
|
||||||
|
statusCode = response.StatusCode;
|
||||||
|
return reader.ReadToEnd();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public T JSONApiCall<T>(string method, string path, Dictionary<string, string> parameters)
|
||||||
|
where T : class
|
||||||
|
{
|
||||||
|
return JSONApiCall<T>(method, path, parameters, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <param name="timeout">The request timeout, in milliseconds.
|
||||||
|
/// Specify 0 to use the system-default timeout. Use caution if
|
||||||
|
/// you choose to specify a custom timeout - some API
|
||||||
|
/// calls (particularly in the Auth APIs) will not
|
||||||
|
/// return a response until an out-of-band authentication process
|
||||||
|
/// has completed. In some cases, this may take as much as a
|
||||||
|
/// small number of minutes.</param>
|
||||||
|
public T JSONApiCall<T>(string method, string path, Dictionary<string, string> parameters, int timeout)
|
||||||
|
where T : class
|
||||||
|
{
|
||||||
|
var res = ApiCall(method, path, parameters, timeout, out var statusCode);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var dict = JsonConvert.DeserializeObject<Dictionary<string, object>>(res);
|
||||||
|
if(dict["stat"] as string == "OK")
|
||||||
|
{
|
||||||
|
return dict["response"] as T;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var check = dict["code"] as int?;
|
||||||
|
var code = check.GetValueOrDefault(0);
|
||||||
|
var messageDetail = string.Empty;
|
||||||
|
if(dict.ContainsKey("message_detail"))
|
||||||
|
{
|
||||||
|
messageDetail = dict["message_detail"] as string;
|
||||||
|
}
|
||||||
|
throw new ApiException(code, (int)statusCode, dict["message"] as string, messageDetail);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(ApiException)
|
||||||
|
{
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
catch(Exception e)
|
||||||
|
{
|
||||||
|
throw new BadResponseException((int)statusCode, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string HmacSign(string data)
|
||||||
|
{
|
||||||
|
var keyBytes = Encoding.ASCII.GetBytes(_skey);
|
||||||
|
var dataBytes = Encoding.ASCII.GetBytes(data);
|
||||||
|
|
||||||
|
using(var hmac = new HMACSHA1(keyBytes))
|
||||||
|
{
|
||||||
|
var hash = hmac.ComputeHash(dataBytes);
|
||||||
|
var hex = BitConverter.ToString(hash);
|
||||||
|
return hex.Replace("-", string.Empty).ToLower();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string Encode64(string plaintext)
|
||||||
|
{
|
||||||
|
var plaintextBytes = Encoding.ASCII.GetBytes(plaintext);
|
||||||
|
return Convert.ToBase64String(plaintextBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string RFC822UtcNow()
|
||||||
|
{
|
||||||
|
// Can't use the "zzzz" format because it adds a ":"
|
||||||
|
// between the offset's hours and minutes.
|
||||||
|
var dateString = DateTime.UtcNow.ToString("ddd, dd MMM yyyy HH:mm:ss", CultureInfo.InvariantCulture);
|
||||||
|
var offset = 0;
|
||||||
|
var zone = "+" + offset.ToString(CultureInfo.InvariantCulture).PadLeft(2, '0');
|
||||||
|
dateString += " " + zone.PadRight(5, '0');
|
||||||
|
return dateString;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class DuoException : Exception
|
||||||
|
{
|
||||||
|
public int HttpStatus { get; private set; }
|
||||||
|
|
||||||
|
public DuoException(int httpStatus, string message, Exception inner)
|
||||||
|
: base(message, inner)
|
||||||
|
{
|
||||||
|
HttpStatus = httpStatus;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ApiException : DuoException
|
||||||
|
{
|
||||||
|
public int Code { get; private set; }
|
||||||
|
public string ApiMessage { get; private set; }
|
||||||
|
public string ApiMessageDetail { get; private set; }
|
||||||
|
|
||||||
|
public ApiException(int code, int httpStatus, string apiMessage, string apiMessageDetail)
|
||||||
|
: base(httpStatus, FormatMessage(code, apiMessage, apiMessageDetail), null)
|
||||||
|
{
|
||||||
|
Code = code;
|
||||||
|
ApiMessage = apiMessage;
|
||||||
|
ApiMessageDetail = apiMessageDetail;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string FormatMessage(int code, string apiMessage, string apiMessageDetail)
|
||||||
|
{
|
||||||
|
return string.Format("Duo API Error {0}: '{1}' ('{2}')", code, apiMessage, apiMessageDetail);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class BadResponseException : DuoException
|
||||||
|
{
|
||||||
|
public BadResponseException(int httpStatus, Exception inner)
|
||||||
|
: base(httpStatus, FormatMessage(httpStatus, inner), inner)
|
||||||
|
{ }
|
||||||
|
|
||||||
|
private static string FormatMessage(int httpStatus, Exception inner)
|
||||||
|
{
|
||||||
|
var innerMessage = "(null)";
|
||||||
|
if(inner != null)
|
||||||
|
{
|
||||||
|
innerMessage = string.Format("'{0}'", inner.Message);
|
||||||
|
}
|
||||||
|
return string.Format("Got error {0} with HTTP Status {1}", innerMessage, httpStatus);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user