//----------------------------------------------------------------------- // // 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."); } } } }