public static async Task <OAuth2Client> LoadClientAsync(string clientId, LoadClientOptions options) { if ((options & LoadClientOptions.LocalClients) == LoadClientOptions.LocalClients) { if (_redisClient == null) { _redisClient = await Singletons.GetRedisClientAsync(); } string fullyQualifiedClientKey = REDIS_PREFIX_CLIENT + REDIS_PREFIX_SEPARATOR + clientId; bool localClientExists = (await _redisClient.ExistsAsync(new string[] { fullyQualifiedClientKey }) > 0); if (localClientExists) { Dictionary <string, string> clientDictionary = await _redisClient.HashGetAllASync <string, string, string>(fullyQualifiedClientKey); string clientIsCachedAsString = clientDictionary.ContainsKey("cached") ? clientDictionary["cached"] : null; bool clientIsCached = (clientIsCachedAsString != null && clientIsCachedAsString != "0"); string timeCreatedAsString = clientDictionary.ContainsKey("time-created") ? clientDictionary["time-created"] : null; Int64? timeCreatedInUnixMicroseconds = null; Int64 timeCreatedAsInt64; if (timeCreatedAsString != null && Int64.TryParse(timeCreatedAsString, out timeCreatedAsInt64)) { timeCreatedInUnixMicroseconds = timeCreatedAsInt64; } string timeUpdatedAsString = clientDictionary.ContainsKey("time-updated") ? clientDictionary["time-updated"] : null; Int64? timeUpdatedInUnixMicroseconds = null; Int64 timeUpdatedAsInt64; if (timeUpdatedAsString != null && Int64.TryParse(timeUpdatedAsString, out timeUpdatedAsInt64)) { timeUpdatedInUnixMicroseconds = timeUpdatedAsInt64; } OAuth2Client resultClient = new OAuth2Client(); resultClient._id = clientId; ParsingHelper.ServerDetails?loginServerDetails = ParsingHelper.ExtractServerDetailsFromAccountServerIdIdentifier(clientId); if (loginServerDetails == null) { throw new Exception(); } resultClient._loginServerDetails = loginServerDetails.Value; // resultClient._accountId = clientDictionary.ContainsKey("account-id") ? clientDictionary["account-id"] : null; // if (clientDictionary.ContainsKey("issued-at")) { long issuedAtAsLong; if (long.TryParse(clientDictionary["issued-at"], out issuedAtAsLong)) { resultClient._issuedAt = DateTimeOffset.FromUnixTimeSeconds(issuedAtAsLong); } } // if (clientDictionary.ContainsKey("secret-hash")) { // load the base64-encoded binary hash of the client secret resultClient._secret = clientDictionary["secret-hash"]; resultClient._secretIsHashed = true; } else { resultClient._secret = null; resultClient._secretIsHashed = false; } // if (resultClient._secret != null) { if (clientDictionary.ContainsKey("expires-at")) { long expiresAtAsLong; if (long.TryParse(clientDictionary["expires-at"], out expiresAtAsLong)) { resultClient._expiresAt = DateTimeOffset.FromUnixTimeSeconds(expiresAtAsLong); } } } // resultClient._softwareId = clientDictionary.ContainsKey("software-id") ? clientDictionary["software-id"] : null; // resultClient._softwareVersion = clientDictionary.ContainsKey("software-version") ? clientDictionary["software-version"] : null; // if (!clientDictionary.ContainsKey("token-endpoint-auth-method")) { // this field is required; return null if it is not present. return(null); } resultClient._tokenEndpointAuthMethod = OAuth2Convert.ConvertStringToTokenEndpointAuthMethod(clientDictionary["token-endpoint-auth-method"]).Value; // resultClient._redirectUris = await _redisClient.SetMembersAsync <string, string>(fullyQualifiedClientKey + REDIS_SUFFIX_SEPARATOR + REDIS_SUFFIX_REDIRECT_URIS).ConfigureAwait(false); // List <string> grantTypesAsStrings = await _redisClient.SetMembersAsync <string, string>(fullyQualifiedClientKey + REDIS_SUFFIX_SEPARATOR + REDIS_SUFFIX_GRANT_TYPES).ConfigureAwait(false); if (grantTypesAsStrings != null) { resultClient._grantTypes = new ListWithDirtyFlag <OAuth2GrantType>(); foreach (string grantTypeAsString in grantTypesAsStrings) { resultClient._grantTypes.Add(OAuth2Convert.ConvertStringToGrantType(grantTypeAsString).Value); } } // List <string> responseTypesAsStrings = await _redisClient.SetMembersAsync <string, string>(fullyQualifiedClientKey + REDIS_SUFFIX_SEPARATOR + REDIS_SUFFIX_RESPONSE_TYPES).ConfigureAwait(false); if (responseTypesAsStrings != null) { resultClient._responseTypes = new ListWithDirtyFlag <OAuth2ResponseType>(); foreach (string responseTypeAsString in responseTypesAsStrings) { resultClient._responseTypes.Add(OAuth2Convert.ConvertStringToResponseType(responseTypeAsString).Value); } } resultClient._scopes = await _redisClient.SetMembersAsync <string, string>(fullyQualifiedClientKey + REDIS_SUFFIX_SEPARATOR + REDIS_SUFFIX_SCOPES).ConfigureAwait(false); // if (clientDictionary.ContainsKey("registration-token-hash")) { // load the base64-encoded binary hash of the client registration token resultClient._registrationToken = clientDictionary["registration-token-hash"]; resultClient._registrationTokenIsHashed = true; } else { resultClient._registrationToken = null; resultClient._registrationTokenIsHashed = false; } resultClient._isCached = clientIsCached; resultClient._timeCreatedInUnixMicroseconds = timeCreatedInUnixMicroseconds; resultClient._timeUpdatedInUnixMicroseconds = timeUpdatedInUnixMicroseconds; return(resultClient); } } // client could not be found return(null); }
public async Task SaveClientAsync() { // we only support saving a local client (i.e. not updating a remote client) if (_isCached) { throw new InvalidOperationException(); } if (_redisClient == null) { _redisClient = await Singletons.GetRedisClientAsync(); } bool objectIsNew = (_timeCreatedInUnixMicroseconds == null); int RESULT_KEY_CONFLICT = -1; int RESULT_DATA_CORRUPTION = -2; int RESULT_UPDATED_SINCE_LOAD = -3; // get current server time long newTimeUpdatedInUnixMicroseconds = await _redisClient.TimeAsync(); if (newTimeUpdatedInUnixMicroseconds < 0) { throw new Exception("Critical Redis error!"); } if (newTimeUpdatedInUnixMicroseconds < _timeUpdatedInUnixMicroseconds) { throw new Exception("Critical Redis error!"); } if (objectIsNew) { // assign clientId, clientSecret, issuedAt time and clientRefreshToken // for non-implicit grant types: generate a clientSecret if ((_grantTypes.Contains(OAuth2GrantType.AuthorizationCode) && _tokenEndpointAuthMethod != OAuth2TokenEndpointAuthMethod.None) || _grantTypes.Contains(OAuth2GrantType.ClientCredentials) || _grantTypes.Contains(OAuth2GrantType.Password)) { _secret = new string(RandomHelper.CreateRandomCharacterSequence_Readable6bit_ForIdentifiers(32)); _secret_IsDirty = true; _expiresAt = null; _expiresAt_IsDirty = true; } _issuedAt = DateTimeOffset.UtcNow; // create client registration token (32-byte == 192-bit) /* NOTE: if we ever want to look up the registration token in the #oauth2tokens collections, we will need to start making sure the token is unique-for-server here */ _registrationToken = _loginServerDetails.ToAccountIdServerIdIdentifierString() + "-" + (new string(RandomHelper.CreateRandomCharacterSequence_Readable6bit_ForIdentifiers(32))); _registrationToken_IsDirty = true; } // generate Lua script (which we will use to commit all changes--or the new record--in an atomic transaction) StringBuilder luaBuilder = new StringBuilder(); List <string> arguments = new List <string>(); int iArgument = 1; if (objectIsNew) { // for new clients: if a client with this client-id already exists, return 0...and we will try again. luaBuilder.Append( "if redis.call(\"EXISTS\", KEYS[1]) == 1 then\n" + " return " + RESULT_KEY_CONFLICT.ToString() + "\n" + "end\n"); } else { // for updated: make sure that the "time-created" timestamp has not changed (i.e. that a new key has not replaced the old key) luaBuilder.Append("local time_created = redis.call(\"HGET\", KEYS[1], \"time-created\")\n"); luaBuilder.Append("if time_created ~= ARGV[" + iArgument.ToString() + "] then\n" + " return " + RESULT_KEY_CONFLICT.ToString() + "\n" + "end\n"); arguments.Add(_timeCreatedInUnixMicroseconds.ToString()); iArgument++; // for updates: make sure that our old "time-updated" timestamp has not changed luaBuilder.Append("local old_time_updated = redis.call(\"HGET\", KEYS[1], \"time-updated\")\n"); luaBuilder.Append("if old_time_updated ~= ARGV[" + iArgument.ToString() + "] then\n" + " return " + RESULT_UPDATED_SINCE_LOAD.ToString() + "\n" + "end\n"); arguments.Add(_timeUpdatedInUnixMicroseconds.ToString()); iArgument++; } // if (objectIsNew) { luaBuilder.Append( "if redis.call(\"HSET\", KEYS[1], \"time-created\", ARGV[" + iArgument.ToString() + "]) == 0 then\n" + " return " + RESULT_DATA_CORRUPTION.ToString() + "\n" + "end\n"); arguments.Add(newTimeUpdatedInUnixMicroseconds.ToString()); iArgument++; } // luaBuilder.Append( "if redis.call(\"HSET\", KEYS[1], \"time-updated\", ARGV[" + iArgument.ToString() + "]) == 0 then\n" + " return " + RESULT_DATA_CORRUPTION.ToString() + "\n" + "end\n"); arguments.Add(newTimeUpdatedInUnixMicroseconds.ToString()); iArgument++; // if (_accountId_IsDirty) { if (_accountId != null) { // if there is an account assigned to this token, save it. luaBuilder.Append( "if redis.call(\"HSET\", KEYS[1], \"account-id\", ARGV[" + iArgument.ToString() + "]) == 0 then\n" + (objectIsNew ? " redis.call(\"DEL\", KEYS[1])\n" : "") + " return " + RESULT_DATA_CORRUPTION.ToString() + "\n" + "end\n"); arguments.Add(_accountId); iArgument++; } else { // if the account-id has been removed, delete it. luaBuilder.Append("redis.call(\"HDEL\", KEYS[1], \"account-id\")\n"); } // clear the dirty flag _accountId_IsDirty = false; } if (_secret_IsDirty) { if (_secret != null) { // if there is a secret assigned to this client, save it. luaBuilder.Append( "if redis.call(\"HSET\", KEYS[1], \"secret-hash\", ARGV[" + iArgument.ToString() + "]) == 0 then\n" + (objectIsNew ? " redis.call(\"DEL\", KEYS[1])\n" : "") + " return " + RESULT_DATA_CORRUPTION.ToString() + "\n" + "end\n"); arguments.Add(Convert.ToBase64String(HashClientSecret(Encoding.UTF8.GetBytes(_secret)))); iArgument++; } else { // if the secret has been removed, delete it. luaBuilder.Append("redis.call(\"HDEL\", KEYS[1], \"secret-hash\")\n"); } // clear the dirty flag _secret_IsDirty = false; } if (objectIsNew) { // set the issued-at time luaBuilder.Append( "if redis.call(\"HSET\", KEYS[1], \"issued-at\", ARGV[" + iArgument.ToString() + "]) == 0 then\n" + (objectIsNew ? " redis.call(\"DEL\", KEYS[1])\n" : "") + " return " + RESULT_DATA_CORRUPTION.ToString() + "\n" + "end\n"); arguments.Add(_issuedAt.Value.ToUnixTimeSeconds().ToString()); iArgument++; } // set the expires-at time if (_expiresAt_IsDirty) { if (_expiresAt != null) { long expiresAtAsLong = _expiresAt.Value.ToUnixTimeSeconds(); luaBuilder.Append( "if redis.call(\"HSET\", KEYS[1], \"expires-at\", ARGV[" + iArgument.ToString() + "]) == 0 then\n" + (objectIsNew ? " redis.call(\"DEL\", KEYS[1])\n" : "") + " return " + RESULT_DATA_CORRUPTION.ToString() + "\n" + "end\n"); arguments.Add(expiresAtAsLong.ToString()); iArgument++; } else { // if the expiration has been removed, delete it. luaBuilder.Append("redis.call(\"HDEL\", KEYS[1], \"expires-at\")\n"); } // clear the dirty flag _expiresAt_IsDirty = false; } if (_softwareId_IsDirty) { luaBuilder.Append( "if redis.call(\"HSET\", KEYS[1], \"software-id\", ARGV[" + iArgument.ToString() + "]) == 0 then\n" + (objectIsNew ? " redis.call(\"DEL\", KEYS[1])\n" : "") + " return " + RESULT_DATA_CORRUPTION.ToString() + "\n" + "end\n"); arguments.Add(_softwareId); iArgument++; // clear the dirty flag _softwareId_IsDirty = false; } if (_softwareVersion_IsDirty) { if (_softwareVersion != null) { // set the softwareVersion luaBuilder.Append( "if redis.call(\"HSET\", KEYS[1], \"software-version\", ARGV[" + iArgument.ToString() + "]) == 0 then\n" + (objectIsNew ? " redis.call(\"DEL\", KEYS[1])\n" : "") + " return " + RESULT_DATA_CORRUPTION.ToString() + "\n" + "end\n"); arguments.Add(_softwareVersion); iArgument++; } else { // if the software-version has been removed, delete it. luaBuilder.Append("redis.call(\"HDEL\", KEYS[1], \"software-version\")\n"); } // clear the dirty flag _softwareVersion_IsDirty = false; } // set the tokenEndpointAuthMethod if (_tokenEndpointAuthMethod_IsDirty) { luaBuilder.Append( "if redis.call(\"HSET\", KEYS[1], \"token-endpoint-auth-method\", ARGV[" + iArgument.ToString() + "]) == 0 then\n" + (objectIsNew ? " redis.call(\"DEL\", KEYS[1])\n" : "") + " return " + RESULT_DATA_CORRUPTION.ToString() + "\n" + "end\n"); arguments.Add(OAuth2Convert.ConvertTokenEndpointAuthMethodToString(_tokenEndpointAuthMethod)); iArgument++; // clear the dirty flag _tokenEndpointAuthMethod_IsDirty = false; } //populate the set of redirect-uris if (_redirectUris.IsDirty) { luaBuilder.Append(objectIsNew ? "" : "redis.call(\"DEL\", KEYS[2])\n"); foreach (string redirectUri in _redirectUris) { luaBuilder.Append( "if redis.call(\"SADD\", KEYS[2], ARGV[" + iArgument.ToString() + "]) == 0 then\n" + (objectIsNew ? " redis.call(\"DEL\", KEYS[1])\n" : "") + (objectIsNew ? " redis.call(\"DEL\", KEYS[2])\n" : "") + " return " + RESULT_DATA_CORRUPTION.ToString() + "\n" + "end\n"); arguments.Add(redirectUri); iArgument++; } // clear the dirty flag _redirectUris.IsDirty = false; } // populate the set of grant-types if (_grantTypes.IsDirty) { luaBuilder.Append(objectIsNew ? "" : "redis.call(\"DEL\", KEYS[3])\n"); foreach (OAuth2GrantType grantType in _grantTypes) { luaBuilder.Append( "if redis.call(\"SADD\", KEYS[3], ARGV[" + iArgument.ToString() + "]) == 0 then\n" + (objectIsNew ? " redis.call(\"DEL\", KEYS[1])\n" : "") + (objectIsNew ? " redis.call(\"DEL\", KEYS[2])\n" : "") + (objectIsNew ? " redis.call(\"DEL\", KEYS[3])\n" : "") + " return " + RESULT_DATA_CORRUPTION.ToString() + "\n" + "end\n"); arguments.Add(OAuth2Convert.ConvertGrantTypeToString(grantType)); iArgument++; } // clear the dirty flag _grantTypes.IsDirty = false; } // populate the set of response-types if (_responseTypes.IsDirty) { luaBuilder.Append(objectIsNew ? "" : "redis.call(\"DEL\", KEYS[4])\n"); foreach (OAuth2ResponseType responseType in _responseTypes) { luaBuilder.Append( "if redis.call(\"SADD\", KEYS[4], ARGV[" + iArgument.ToString() + "]) == 0 then\n" + (objectIsNew ? " redis.call(\"DEL\", KEYS[1])\n" : "") + (objectIsNew ? " redis.call(\"DEL\", KEYS[2])\n" : "") + (objectIsNew ? " redis.call(\"DEL\", KEYS[3])\n" : "") + (objectIsNew ? " redis.call(\"DEL\", KEYS[4])\n" : "") + " return " + RESULT_DATA_CORRUPTION.ToString() + "\n" + "end\n"); arguments.Add(OAuth2Convert.ConvertResponseTypeToString(responseType)); iArgument++; } // clear the dirty flag _responseTypes.IsDirty = false; } // populate the set of scopes if (_scopes.IsDirty) { luaBuilder.Append(objectIsNew ? "" : "redis.call(\"DEL\", KEYS[5])\n"); foreach (string scope in _scopes) { luaBuilder.Append( "if redis.call(\"SADD\", KEYS[5], ARGV[" + iArgument.ToString() + "]) == 0 then\n" + (objectIsNew ? " redis.call(\"DEL\", KEYS[1])\n" : "") + (objectIsNew ? " redis.call(\"DEL\", KEYS[2])\n" : "") + (objectIsNew ? " redis.call(\"DEL\", KEYS[3])\n" : "") + (objectIsNew ? " redis.call(\"DEL\", KEYS[4])\n" : "") + (objectIsNew ? " redis.call(\"DEL\", KEYS[5])\n" : "") + " return " + RESULT_DATA_CORRUPTION.ToString() + "\n" + "end\n"); arguments.Add(scope); iArgument++; } // clear the dirty flag _scopes.IsDirty = false; } // if (_registrationToken_IsDirty) { if (_registrationToken != null) { // if there is a registration token assigned to this client, save it. luaBuilder.Append( "if redis.call(\"HSET\", KEYS[1], \"registration-token-hash\", ARGV[" + iArgument.ToString() + "]) == 0 then\n" + (objectIsNew ? " redis.call(\"DEL\", KEYS[1])\n" : "") + " return " + RESULT_DATA_CORRUPTION.ToString() + "\n" + "end\n"); arguments.Add(Convert.ToBase64String(HashClientRegistrationToken(Encoding.UTF8.GetBytes(_registrationToken)))); iArgument++; } else { // if the registration token has been removed, delete it. // NOTE: this operation is technically supported by spec--but removing the ability for a client to manage a token (usually because of third-party meddling) can have some unfortunate consequences as well luaBuilder.Append("redis.call(\"HDEL\", KEYS[1], \"registration-token-hash\")\n"); } // clear the dirty flag _registrationToken_IsDirty = false; } // luaBuilder.Append("return 1\n"); long luaResult = 0; for (int iRetry = 0; iRetry < 1000; iRetry++) { if (objectIsNew) { // generate a 32-byte (192-bit) client_id _id = _loginServerDetails.ToAccountIdServerIdIdentifierString() + "-" + (new string(RandomHelper.CreateRandomCharacterSequence_Readable6bit_ForIdentifiers(32))); } List <string> keys = new List <string>(); keys.Add(REDIS_PREFIX_CLIENT + REDIS_PREFIX_SEPARATOR + _id); keys.Add(REDIS_PREFIX_CLIENT + REDIS_PREFIX_SEPARATOR + _id + REDIS_SUFFIX_SEPARATOR + REDIS_SUFFIX_REDIRECT_URIS); keys.Add(REDIS_PREFIX_CLIENT + REDIS_PREFIX_SEPARATOR + _id + REDIS_SUFFIX_SEPARATOR + REDIS_SUFFIX_GRANT_TYPES); keys.Add(REDIS_PREFIX_CLIENT + REDIS_PREFIX_SEPARATOR + _id + REDIS_SUFFIX_SEPARATOR + REDIS_SUFFIX_RESPONSE_TYPES); keys.Add(REDIS_PREFIX_CLIENT + REDIS_PREFIX_SEPARATOR + _id + REDIS_SUFFIX_SEPARATOR + REDIS_SUFFIX_SCOPES); luaResult = await _redisClient.EvalAsync <string, string, long>(luaBuilder.ToString(), keys.ToArray(), arguments.ToArray()).ConfigureAwait(false); // NOTE: the result will contain a negative integer (error) or one (success) // if we were able to create a key, break out of this loop; otherwise, try generating new keys up to ten times. if (luaResult == 1) { // save our "time-updated" timestamp _timeUpdatedInUnixMicroseconds = newTimeUpdatedInUnixMicroseconds; if (objectIsNew) { // save our "time-created" timestamp _timeCreatedInUnixMicroseconds = newTimeUpdatedInUnixMicroseconds; if (_accountId == null) { // if the client belongs to the entire system (and not to an account), add it to the root client collection. await _redisClient.SetAddAsync <string, string>(REDIS_PREFIX_LOGIN_SERVICE + REDIS_PREFIX_SEPARATOR + REDIS_ASTERISK + REDIS_SLASH + _loginServerDetails.ServerId + REDIS_SUFFIX_SEPARATOR + REDIS_SUFFIX_OAUTH2CLIENTS, new string[] { _id }); } else { // if the client belongs to the account (and not to the user), add it to the account's client collection. await _redisClient.SetAddAsync <string, string>(REDIS_PREFIX_LOGIN_SERVICE + REDIS_PREFIX_SEPARATOR + _accountId + REDIS_SLASH + _loginServerDetails.ServerId + REDIS_SUFFIX_SEPARATOR + REDIS_SUFFIX_OAUTH2CLIENTS, new string[] { _id }); } } break; } else if (luaResult == RESULT_KEY_CONFLICT) { // key name conflict; try again } else if (luaResult == RESULT_DATA_CORRUPTION) { // data corruption throw new Exception("Critical Redis error!"); } else if (luaResult == RESULT_UPDATED_SINCE_LOAD) { // token was updated since we loaded it; we need to reload the token, make the changes again, and then attempt to save it again throw new Exception("Critical Redis error!"); } else { // unknown error throw new Exception("Critical Redis error!"); } } if (luaResult < 0) { throw new Exception("Critical Redis error!"); } }