//-----------------------------------------------------------------------
//
// Copyright (c) Outercurve Foundation. All rights reserved.
//
//-----------------------------------------------------------------------
namespace DotNetOpenAuth.ApplicationBlock {
using System;
using System.Collections.Specialized;
using System.Globalization;
using System.IO;
using System.Net;
using System.Text;
using System.Text.RegularExpressions;
///
/// The set of possible results from verifying a Yubikey token.
///
public enum YubikeyResult {
///
/// The OTP is valid.
///
Ok,
///
/// The OTP is invalid format.
///
BadOtp,
///
/// The OTP has already been seen by the service.
///
ReplayedOtp,
///
/// The HMAC signature verification failed.
///
///
/// This indicates a bug in the relying party code.
///
BadSignature,
///
/// The request lacks a parameter.
///
///
/// This indicates a bug in the relying party code.
///
MissingParameter,
///
/// The request id does not exist.
///
NoSuchClient,
///
/// The request id is not allowed to verify OTPs.
///
OperationNotAllowed,
///
/// Unexpected error in our server. Please contact Yubico if you see this error.
///
BackendError,
}
///
/// Provides verification of a Yubikey one-time password (OTP) as a means of authenticating
/// a user at your web site or application.
///
///
/// Please visit http://yubico.com/ for more information about this authentication method.
///
public class YubikeyRelyingParty {
///
/// The default Yubico authorization server to use for validation and replay protection.
///
private const string DefaultYubicoAuthorizationServer = "https://api.yubico.com/wsapi/verify";
///
/// The format of the lines in the Yubico server response.
///
private static readonly Regex ResultLineMatcher = new Regex(@"^(?[^=]+)=(?.*)$");
///
/// The Yubico authorization server to use for validation and replay protection.
///
private readonly string yubicoAuthorizationServer;
///
/// The authorization ID assigned to your individual site by Yubico.
///
private readonly int yubicoAuthorizationId;
///
/// Initializes a new instance of the class
/// that uses the default Yubico server for validation and replay protection.
///
/// The authorization ID assigned to your individual site by Yubico.
/// Get one from https://upgrade.yubico.com/getapikey/
public YubikeyRelyingParty(int authorizationId)
: this(authorizationId, DefaultYubicoAuthorizationServer) {
}
///
/// Initializes a new instance of the class.
///
/// The authorization ID assigned to your individual site by Yubico.
/// Contact tech@yubico.com if you haven't got an authId for your site.
/// The Yubico authorization server to use for validation and replay protection.
public YubikeyRelyingParty(int authorizationId, string yubicoAuthorizationServer) {
if (authorizationId < 0) {
throw new ArgumentOutOfRangeException("authorizationId");
}
if (!Uri.IsWellFormedUriString(yubicoAuthorizationServer, UriKind.Absolute)) {
throw new ArgumentException("Invalid authorization server URI", "yubicoAuthorizationServer");
}
if (!yubicoAuthorizationServer.StartsWith(Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase)) {
throw new ArgumentException("HTTPS is required for the Yubico server. HMAC response verification not supported.", "yubicoAuthorizationServer");
}
this.yubicoAuthorizationId = authorizationId;
this.yubicoAuthorizationServer = yubicoAuthorizationServer;
}
///
/// Extracts the username out of a Yubikey token.
///
/// The yubikey token.
/// A 12 character string that is unique for this particular Yubikey device.
public static string ExtractUsername(string yubikeyToken) {
EnsureWellFormedToken(yubikeyToken);
return yubikeyToken.Substring(0, 12);
}
///
/// Determines whether the specified yubikey token is valid and has not yet been used.
///
/// The yubikey token.
///
/// true if the specified yubikey token is valid; otherwise, false.
///
/// Thrown when the validity of the token could not be confirmed due to network issues.
public YubikeyResult IsValid(string yubikeyToken) {
EnsureWellFormedToken(yubikeyToken);
StringBuilder authorizationUri = new StringBuilder(this.yubicoAuthorizationServer);
authorizationUri.Append("?id=");
authorizationUri.Append(Uri.EscapeDataString(this.yubicoAuthorizationId.ToString(CultureInfo.InvariantCulture)));
authorizationUri.Append("&otp=");
authorizationUri.Append(Uri.EscapeDataString(yubikeyToken));
var request = WebRequest.Create(authorizationUri.ToString());
using (var response = request.GetResponse()) {
using (var responseReader = new StreamReader(response.GetResponseStream())) {
string line;
var result = new NameValueCollection();
while ((line = responseReader.ReadLine()) != null) {
Match m = ResultLineMatcher.Match(line);
if (m.Success) {
result[m.Groups["key"].Value] = m.Groups["value"].Value;
}
}
return ParseResult(result["status"]);
}
}
}
///
/// Parses the Yubico server result.
///
/// The status field from the response.
/// The enum value representing the result.
private static YubikeyResult ParseResult(string status) {
switch (status) {
case "OK": return YubikeyResult.Ok;
case "BAD_OTP": return YubikeyResult.BadOtp;
case "REPLAYED_OTP": return YubikeyResult.ReplayedOtp;
case "BAD_SIGNATURE": return YubikeyResult.BadSignature;
case "MISSING_PARAMETER": return YubikeyResult.MissingParameter;
case "NO_SUCH_CLIENT": return YubikeyResult.NoSuchClient;
case "OPERATION_NOT_ALLOWED": return YubikeyResult.OperationNotAllowed;
case "BACKEND_ERROR": return YubikeyResult.BackendError;
default: throw new ArgumentOutOfRangeException("status", status, "Unexpected status value.");
}
}
///
/// Ensures the OTP is well formed.
///
/// The yubikey token.
private static void EnsureWellFormedToken(string yubikeyToken) {
if (yubikeyToken == null) {
throw new ArgumentNullException("yubikeyToken");
}
yubikeyToken = yubikeyToken.Trim();
if (yubikeyToken.Length <= 12) {
throw new ArgumentException("Yubikey token has unexpected length.");
}
}
}
}