forked from DotNetOpenAuth/DotNetOpenAuth
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathYubikeyRelyingParty.cs
207 lines (180 loc) · 7.25 KB
/
YubikeyRelyingParty.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
//-----------------------------------------------------------------------
// <copyright file="YubikeyRelyingParty.cs" company="Outercurve Foundation">
// Copyright (c) Outercurve Foundation. All rights reserved.
// </copyright>
//-----------------------------------------------------------------------
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;
/// <summary>
/// The set of possible results from verifying a Yubikey token.
/// </summary>
public enum YubikeyResult {
/// <summary>
/// The OTP is valid.
/// </summary>
Ok,
/// <summary>
/// The OTP is invalid format.
/// </summary>
BadOtp,
/// <summary>
/// The OTP has already been seen by the service.
/// </summary>
ReplayedOtp,
/// <summary>
/// The HMAC signature verification failed.
/// </summary>
/// <remarks>
/// This indicates a bug in the relying party code.
/// </remarks>
BadSignature,
/// <summary>
/// The request lacks a parameter.
/// </summary>
/// <remarks>
/// This indicates a bug in the relying party code.
/// </remarks>
MissingParameter,
/// <summary>
/// The request id does not exist.
/// </summary>
NoSuchClient,
/// <summary>
/// The request id is not allowed to verify OTPs.
/// </summary>
OperationNotAllowed,
/// <summary>
/// Unexpected error in our server. Please contact Yubico if you see this error.
/// </summary>
BackendError,
}
/// <summary>
/// Provides verification of a Yubikey one-time password (OTP) as a means of authenticating
/// a user at your web site or application.
/// </summary>
/// <remarks>
/// Please visit http://yubico.com/ for more information about this authentication method.
/// </remarks>
public class YubikeyRelyingParty {
/// <summary>
/// The default Yubico authorization server to use for validation and replay protection.
/// </summary>
private const string DefaultYubicoAuthorizationServer = "https://api.yubico.com/wsapi/verify";
/// <summary>
/// The format of the lines in the Yubico server response.
/// </summary>
private static readonly Regex ResultLineMatcher = new Regex(@"^(?<key>[^=]+)=(?<value>.*)$");
/// <summary>
/// The Yubico authorization server to use for validation and replay protection.
/// </summary>
private readonly string yubicoAuthorizationServer;
/// <summary>
/// The authorization ID assigned to your individual site by Yubico.
/// </summary>
private readonly int yubicoAuthorizationId;
/// <summary>
/// Initializes a new instance of the <see cref="YubikeyRelyingParty"/> class
/// that uses the default Yubico server for validation and replay protection.
/// </summary>
/// <param name="authorizationId">The authorization ID assigned to your individual site by Yubico.
/// Get one from https://upgrade.yubico.com/getapikey/</param>
public YubikeyRelyingParty(int authorizationId)
: this(authorizationId, DefaultYubicoAuthorizationServer) {
}
/// <summary>
/// Initializes a new instance of the <see cref="YubikeyRelyingParty"/> class.
/// </summary>
/// <param name="authorizationId">The authorization ID assigned to your individual site by Yubico.
/// Contact [email protected] if you haven't got an authId for your site.</param>
/// <param name="yubicoAuthorizationServer">The Yubico authorization server to use for validation and replay protection.</param>
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;
}
/// <summary>
/// Extracts the username out of a Yubikey token.
/// </summary>
/// <param name="yubikeyToken">The yubikey token.</param>
/// <returns>A 12 character string that is unique for this particular Yubikey device.</returns>
public static string ExtractUsername(string yubikeyToken) {
EnsureWellFormedToken(yubikeyToken);
return yubikeyToken.Substring(0, 12);
}
/// <summary>
/// Determines whether the specified yubikey token is valid and has not yet been used.
/// </summary>
/// <param name="yubikeyToken">The yubikey token.</param>
/// <returns>
/// <c>true</c> if the specified yubikey token is valid; otherwise, <c>false</c>.
/// </returns>
/// <exception cref="WebException">Thrown when the validity of the token could not be confirmed due to network issues.</exception>
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"]);
}
}
}
/// <summary>
/// Parses the Yubico server result.
/// </summary>
/// <param name="status">The status field from the response.</param>
/// <returns>The enum value representing the result.</returns>
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.");
}
}
/// <summary>
/// Ensures the OTP is well formed.
/// </summary>
/// <param name="yubikeyToken">The yubikey token.</param>
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.");
}
}
}
}