1
0
Fork 0
arangodb/js/server/modules/@arangodb/foxx/authentication.js

1148 lines
30 KiB
JavaScript

'use strict';
////////////////////////////////////////////////////////////////////////////////
/// @brief Foxx Authentication
///
/// @file
///
/// DISCLAIMER
///
/// Copyright 2013 triagens GmbH, Cologne, Germany
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
/// Copyright holder is triAGENS GmbH, Cologne, Germany
///
/// @author Jan Steemann, Lucas Dohmen
/// @author Copyright 2013, triAGENS GmbH, Cologne, Germany
////////////////////////////////////////////////////////////////////////////////
var db = require("@arangodb").db,
crypto = require("@arangodb/crypto"),
internal = require("internal"),
is = require("@arangodb/is"),
_ = require("lodash"),
errors = internal.errors,
defaultsFor = {};
function createAuthenticationMiddleware(auth, applicationContext) {
return function (req) {
var users = new Users(applicationContext),
authResult = auth.authenticate(req);
if (authResult.errorNum === errors.ERROR_NO_ERROR.code) {
req.currentSession = authResult.session;
req.user = users.get(authResult.session.identifier);
} else {
req.currentSession = null;
req.user = null;
}
};
}
function createSessionUpdateMiddleware() {
return function (req) {
var session = req.currentSession;
if (is.existy(session)) {
session.update();
}
};
}
function createAuthObject(applicationContext, opts) {
var sessions,
cookieAuth,
auth,
options = opts || {};
checkAuthenticationOptions(options);
sessions = new Sessions(applicationContext, options);
cookieAuth = new CookieAuthentication(applicationContext, options);
auth = new Authentication(applicationContext, sessions, cookieAuth);
return auth;
}
function checkAuthenticationOptions(options) {
if (options.type !== "cookie") {
throw new Error("Currently only the following auth types are supported: cookie");
}
if (is.falsy(options.cookieLifetime)) {
throw new Error("Please provide the cookieLifetime");
}
if (is.falsy(options.sessionLifetime)) {
throw new Error("Please provide the sessionLifetime");
}
}
defaultsFor.login = {
usernameField: "username",
passwordField: "password",
onSuccess: function (req, res) {
res.json({
user: req.user.identifier,
key: req.currentSession._key
});
},
onError: function (req, res) {
res.status(401);
res.json({
error: "Username or Password was wrong"
});
}
};
function createStandardLoginHandler(auth, users, opts) {
var options = _.defaults(opts || {}, defaultsFor.login);
return function (req, res) {
var username = req.body()[options.usernameField],
password = req.body()[options.passwordField];
if (users.isValid(username, password)) {
req.currentSession = auth.beginSession(req, res, username, {});
req.user = users.get(req.currentSession.identifier);
options.onSuccess(req, res);
} else {
options.onError(req, res);
}
};
}
defaultsFor.logout = {
onSuccess: function (req, res) {
res.json({
notice: "Logged out!"
});
},
onError: function (req, res) {
res.status(401);
res.json({
error: "No session was found"
});
}
};
function createStandardLogoutHandler(auth, opts) {
var options = _.defaults(opts || {}, defaultsFor.logout);
return function (req, res) {
if (is.existy(req.currentSession)) {
auth.endSession(req, res, req.currentSession._key);
req.user = null;
req.currentSession = null;
options.onSuccess(req, res);
} else {
options.onError(req, res);
}
};
}
defaultsFor.registration = {
usernameField: "username",
passwordField: "password",
acceptedAttributes: [],
defaultAttributes: {},
onSuccess: function (req, res) {
res.json({
user: req.user
});
},
onError: function (req, res) {
res.status(401);
res.json({
error: "Registration failed"
});
}
};
function createStandardRegistrationHandler(auth, users, opts) {
var options = _.defaults(opts || {}, defaultsFor.registration);
return function (req, res) {
var username = req.body()[options.usernameField],
password = req.body()[options.passwordField],
data = _.defaults({}, options.defaultAttributes);
options.acceptedAttributes.forEach(function (attributeName) {
var val = req.body()[attributeName];
if (is.existy(val)) {
data[attributeName] = val;
}
});
if (users.exists(username)) {
throw new UserAlreadyExistsError();
}
req.user = users.add(username, password, true, data);
req.currentSession = auth.beginSession(req, res, username, {});
options.onSuccess(req, res);
};
}
defaultsFor.changePassword = {
passwordField: "password",
onSuccess: function (req, res) {
res.json({
notice: "Changed password!"
});
},
onError: function (req, res) {
res.status(401);
res.json({
error: "No session was found"
});
}
};
function createStandardChangePasswordHandler(users, opts) {
var options = _.defaults(opts || {}, defaultsFor.changePassword);
return function (req, res) {
var password = req.body()[options.passwordField],
successfull = false;
if (is.existy(req.currentSession)) {
successfull = users.setPassword(req.currentSession.identifier, password);
if (successfull) {
options.onSuccess(req, res);
} else {
options.onError(req, res);
}
} else {
options.onError(req, res);
}
};
}
////////////////////////////////////////////////////////////////////////////////
/// @brief constructor
////////////////////////////////////////////////////////////////////////////////
function generateToken() {
return internal.genRandomAlphaNumbers(32);
}
////////////////////////////////////////////////////////////////////////////////
/// @brief deep-copies a document
////////////////////////////////////////////////////////////////////////////////
function cloneDocument(obj) {
var copy, a;
if (obj === null || typeof obj !== "object") {
return obj;
}
if (Array.isArray(obj)) {
copy = [];
obj.forEach(function (i) {
copy.push(cloneDocument(i));
});
} else if (obj instanceof Object) {
copy = {};
for (a in obj) {
if (obj.hasOwnProperty(a)) {
copy[a] = cloneDocument(obj[a]);
}
}
}
return copy;
}
////////////////////////////////////////////////////////////////////////////////
/// @brief checks whether the plain text password matches the encoded one
////////////////////////////////////////////////////////////////////////////////
function checkPassword(plain, encoded) {
var salted = encoded.substr(3, 8) + plain,
hex = crypto.sha256(salted);
return (encoded.substr(12) === hex);
}
////////////////////////////////////////////////////////////////////////////////
/// @brief encodes a password
////////////////////////////////////////////////////////////////////////////////
function encodePassword(password) {
var salt,
encoded,
random;
random = crypto.rand();
if (random === undefined) {
random = "time:" + internal.time();
} else {
random = "random:" + random;
}
salt = crypto.sha256(random);
salt = salt.substr(0, 8);
encoded = "$1$" + salt + "$" + crypto.sha256(salt + password);
return encoded;
}
////////////////////////////////////////////////////////////////////////////////
/// @brief constructor
////////////////////////////////////////////////////////////////////////////////
function Users(applicationContext, options) {
this._options = options || {};
this._collection = null;
if (this._options.hasOwnProperty("collectionName")) {
this._collectionName = this._options.collectionName;
} else {
this._collectionName = applicationContext.collectionName("users");
}
}
////////////////////////////////////////////////////////////////////////////////
/// @brief returns the collection
////////////////////////////////////////////////////////////////////////////////
Users.prototype.storage = function () {
if (this._collection === null) {
this._collection = db._collection(this._collectionName);
if (!this._collection) {
throw new Error("users collection not found");
}
}
return this._collection;
};
////////////////////////////////////////////////////////////////////////////////
/// @brief validate a user identifier
////////////////////////////////////////////////////////////////////////////////
Users.prototype._validateIdentifier = function (identifier, allowObject) {
if (allowObject) {
if (typeof identifier === "object" && identifier.hasOwnProperty("identifier")) {
identifier = identifier.identifier;
}
}
if (typeof identifier !== "string") {
throw new TypeError("invalid type for 'identifier'");
}
if (identifier.length === 0) {
throw new Error("invalid user identifier");
}
return identifier;
};
////////////////////////////////////////////////////////////////////////////////
/// @brief sets up the users collection
////////////////////////////////////////////////////////////////////////////////
Users.prototype.setup = function (options) {
var journalSize,
createOptions;
if (typeof options === "object" && options.hasOwnProperty("journalSize")) {
journalSize = options.journalSize;
}
createOptions = {
journalSize : journalSize || 2 * 1024 * 1024
};
if (!db._collection(this._collectionName)) {
db._create(this._collectionName, createOptions);
}
this.storage().ensureIndex({ type: "hash", fields: [ "identifier" ], sparse: true });
};
////////////////////////////////////////////////////////////////////////////////
/// @brief tears down the users collection
////////////////////////////////////////////////////////////////////////////////
Users.prototype.teardown = function () {
var c = db._collection(this._collectionName);
if (c) {
c.drop();
}
};
////////////////////////////////////////////////////////////////////////////////
/// @brief flushes all users
////////////////////////////////////////////////////////////////////////////////
Users.prototype.flush = function () {
this.storage().truncate();
};
////////////////////////////////////////////////////////////////////////////////
/// @brief add a user
////////////////////////////////////////////////////////////////////////////////
Users.prototype.add = function (identifier, password, active, data) {
var c = this.storage(),
user;
identifier = this._validateIdentifier(identifier, false);
if (typeof password !== "string") {
throw new TypeError("invalid type for 'password'");
}
if (active !== undefined && typeof active !== "boolean") {
throw new TypeError("invalid type for 'active'");
}
if (active === undefined) {
active = true;
}
user = {
identifier: identifier,
password: encodePassword(password),
active: active,
data: data || {}
};
db._executeTransaction({
collections: {
write: c.name()
},
action: function (params) {
var c = db._collection(params.cn),
u = c.firstExample({ identifier: params.user.identifier });
if (u === null) {
c.save(params.user);
} else {
c.replace(u._key, params.user);
}
},
params: {
cn: c.name(),
user: user
}
});
delete user.password;
return user;
};
////////////////////////////////////////////////////////////////////////////////
/// @brief update a user
////////////////////////////////////////////////////////////////////////////////
Users.prototype.updateData = function (identifier, data) {
var c = this.storage();
identifier = this._validateIdentifier(identifier, true);
db._executeTransaction({
collections: {
write: c.name()
},
action: function (params) {
var c = db._collection(params.cn),
u = c.firstExample({ identifier: params.identifier });
if (u === null) {
throw new Error("user not found");
}
c.update(u._key, params.data, true, false);
},
params: {
cn: c.name(),
identifier: identifier,
data: data || {}
}
});
return true;
};
////////////////////////////////////////////////////////////////////////////////
/// @brief set activity flag
////////////////////////////////////////////////////////////////////////////////
Users.prototype.setActive = function (identifier, active) {
var c = this.storage(),
user,
doc;
identifier = this._validateIdentifier(identifier, true);
user = c.firstExample({ identifier: identifier });
if (user === null) {
return false;
}
// must clone because shaped json cannot be modified
doc = cloneDocument(user);
doc.active = active;
c.update(doc._key, doc, true, false);
return true;
};
////////////////////////////////////////////////////////////////////////////////
/// @brief set password
////////////////////////////////////////////////////////////////////////////////
Users.prototype.setPassword = function (identifier, password) {
var c = this.storage(),
user,
doc;
identifier = this._validateIdentifier(identifier, true);
if (typeof password !== "string") {
throw new TypeError("invalid type for 'password'");
}
user = c.firstExample({ identifier: identifier });
if (user === null) {
return false;
}
doc = cloneDocument(user);
doc.password = encodePassword(password);
c.update(doc._key, doc, true, false);
return true;
};
////////////////////////////////////////////////////////////////////////////////
/// @brief remove a user
////////////////////////////////////////////////////////////////////////////////
Users.prototype.remove = function (identifier) {
var c = this.storage(),
user;
identifier = this._validateIdentifier(identifier, true);
user = c.firstExample({ identifier: identifier });
if (user === null) {
return false;
}
try {
c.remove(user._key);
} catch (err) {
}
return true;
};
////////////////////////////////////////////////////////////////////////////////
/// @brief returns a user
////////////////////////////////////////////////////////////////////////////////
Users.prototype.get = function (identifier) {
var c = this.storage(),
user;
identifier = this._validateIdentifier(identifier, true);
user = c.firstExample({ identifier: identifier });
if (user === null) {
throw new Error("user not found");
}
delete user.password;
return user;
};
////////////////////////////////////////////////////////////////////////////////
/// @brief checks if a user exists
////////////////////////////////////////////////////////////////////////////////
Users.prototype.exists = function (identifier) {
var c = this.storage(),
user;
identifier = this._validateIdentifier(identifier, true);
user = c.firstExample({ identifier: identifier });
return (user !== null);
};
////////////////////////////////////////////////////////////////////////////////
/// @brief check whether a user is valid
////////////////////////////////////////////////////////////////////////////////
Users.prototype.isValid = function (identifier, password) {
var c = this.storage(),
user;
identifier = this._validateIdentifier(identifier, false);
user = c.firstExample({ identifier: identifier });
if (user === null) {
return false;
}
if (!user.active) {
return false;
}
if (!checkPassword(password, user.password)) {
return false;
}
delete user.password;
return user;
};
////////////////////////////////////////////////////////////////////////////////
/// @brief constructor
////////////////////////////////////////////////////////////////////////////////
function Sessions(applicationContext, options) {
this._applicationContext = applicationContext;
this._options = options || {};
this._collection = null;
if (!this._options.hasOwnProperty("minUpdateResoultion")) {
this._options.minUpdateResolution = 10;
}
if (this._options.hasOwnProperty("collectionName")) {
this._collectionName = this._options.collectionName;
} else {
this._collectionName = applicationContext.collectionName("sessions");
}
}
////////////////////////////////////////////////////////////////////////////////
/// @brief create a session object from a document
////////////////////////////////////////////////////////////////////////////////
Sessions.prototype._toObject = function (session) {
var that = this;
return {
_changed: false,
_key: session._key,
identifier: session.identifier,
expires: session.expires,
data: session.data,
set: function (key, value) {
this.data[key] = value;
this._changed = true;
},
get: function (key) {
return this.data[key];
},
update: function () {
var oldExpires, newExpires;
if (!this._changed) {
oldExpires = this.expires;
newExpires = internal.time() + that._options.sessionLifetime;
if (newExpires - oldExpires > that._options.minUpdateResolution) {
this.expires = newExpires;
this._changed = true;
}
}
if (this._changed) {
that.update(this._key, this.data);
this._changed = false;
}
}
};
};
////////////////////////////////////////////////////////////////////////////////
/// @brief sets up the sessions
////////////////////////////////////////////////////////////////////////////////
Sessions.prototype.setup = function (options) {
var journalSize,
createOptions;
if (typeof options === "object" && options.hasOwnProperty("journalSize")) {
journalSize = options.journalSize;
}
createOptions = {
journalSize : journalSize || 4 * 1024 * 1024
};
if (!db._collection(this._collectionName)) {
db._create(this._collectionName, createOptions);
}
this.storage().ensureIndex({ type: "hash", fields: ["identifier"]});
};
////////////////////////////////////////////////////////////////////////////////
/// @brief tears down the sessions
////////////////////////////////////////////////////////////////////////////////
Sessions.prototype.teardown = function () {
var c = db._collection(this._collectionName);
if (c) {
c.drop();
}
};
////////////////////////////////////////////////////////////////////////////////
/// @brief return the collection
////////////////////////////////////////////////////////////////////////////////
Sessions.prototype.storage = function () {
if (this._collection === null) {
this._collection = db._collection(this._collectionName);
if (!this._collection) {
throw new Error("sessions collection not found");
}
}
return this._collection;
};
////////////////////////////////////////////////////////////////////////////////
/// @brief generate a new session
////////////////////////////////////////////////////////////////////////////////
Sessions.prototype.generate = function (identifier, data) {
var storage, token, session;
if (typeof identifier !== "string" || identifier.length === 0) {
throw new TypeError("invalid type for 'identifier'");
}
if (!this._options.hasOwnProperty("sessionLifetime")) {
throw new Error("no value specified for 'sessionLifetime'");
}
storage = this.storage();
if (!this._options.allowMultiple) {
// remove previous existing sessions
storage.byExample({ identifier: identifier }).toArray().forEach(function (s) {
storage.remove(s);
});
}
while (true) {
token = generateToken();
session = {
_key: token,
expires: internal.time() + this._options.sessionLifetime,
identifier: identifier,
data: data || {}
};
try {
storage.save(session);
return this._toObject(session);
} catch (err) {
// we might have generated the same key again
if (err.hasOwnProperty("errorNum") &&
err.errorNum === internal.errors.ERROR_ARANGO_UNIQUE_CONSTRAINT_VIOLATED.code) {
// duplicate key, try again
continue;
}
throw err;
}
}
};
////////////////////////////////////////////////////////////////////////////////
/// @brief update a session
////////////////////////////////////////////////////////////////////////////////
Sessions.prototype.update = function (token, data) {
this.storage().update(token, {
expires: internal.time() + this._options.sessionLifetime,
data: data
}, true, false);
};
////////////////////////////////////////////////////////////////////////////////
/// @brief terminate a session
////////////////////////////////////////////////////////////////////////////////
Sessions.prototype.terminate = function (token) {
try {
this.storage().remove(token);
} catch (err) {
// some error, e.g. document not found. we don't care
}
};
////////////////////////////////////////////////////////////////////////////////
/// @brief get an existing session
////////////////////////////////////////////////////////////////////////////////
Sessions.prototype.get = function (token) {
var storage = this.storage(),
session;
try {
session = storage.document(token);
if (session.expires >= internal.time()) {
// session still valid
return {
errorNum: internal.errors.ERROR_NO_ERROR.code,
session : this._toObject(session)
};
}
// expired
return {
errorNum: internal.errors.ERROR_SESSION_EXPIRED.code
};
} catch (err) {
// document not found etc.
}
return {
errorNum: internal.errors.ERROR_SESSION_UNKNOWN.code
};
};
////////////////////////////////////////////////////////////////////////////////
/// @brief constructor
////////////////////////////////////////////////////////////////////////////////
function CookieAuthentication(applicationContext, options) {
options = options || {};
this._applicationContext = applicationContext;
this._options = {
name: options.name || this._applicationContext.name + "-session",
cookieLifetime: options.cookieLifetime || 3600,
path: options.path || "/",
domain: options.domain || undefined,
secure: options.secure || false,
httpOnly: options.httpOnly || false
};
this._collectionName = applicationContext.collectionName("sessions");
this._collection = null;
}
////////////////////////////////////////////////////////////////////////////////
/// @brief get a cookie from the request
////////////////////////////////////////////////////////////////////////////////
CookieAuthentication.prototype.getTokenFromRequest = function (req) {
if (!req.hasOwnProperty("cookies")) {
return null;
}
if (!req.cookies.hasOwnProperty(this._options.name)) {
return null;
}
return req.cookies[this._options.name];
};
////////////////////////////////////////////////////////////////////////////////
/// @brief register a cookie in the request
////////////////////////////////////////////////////////////////////////////////
CookieAuthentication.prototype.setCookie = function (res, value) {
var name = this._options.name,
cookie,
i,
n;
cookie = {
name: name,
value: value,
lifeTime: (value === null || value === "") ? 0 : this._options.cookieLifetime,
path: this._options.path,
secure: this._options.secure,
domain: this._options.domain,
httpOnly: this._options.httpOnly
};
if (!res.hasOwnProperty("cookies")) {
res.cookies = [ ];
}
if (!Array.isArray(res.cookies)) {
res.cookies = [ res.cookies ];
}
n = res.cookies.length;
for (i = 0; i < n; ++i) {
if (res.cookies[i].name === name) {
// found existing cookie. overwrite it
res.cookies[i] = cookie;
return;
}
}
res.cookies.push(cookie);
};
////////////////////////////////////////////////////////////////////////////////
/// @brief check whether the request contains authentication data
////////////////////////////////////////////////////////////////////////////////
CookieAuthentication.prototype.getAuthenticationData = function (req) {
var token = this.getTokenFromRequest(req);
if (token === null) {
return null;
}
return {
token: token
};
};
////////////////////////////////////////////////////////////////////////////////
/// @brief generate authentication data
////////////////////////////////////////////////////////////////////////////////
CookieAuthentication.prototype.beginSession = function (req, res, token) {
this.setCookie(res, token);
};
////////////////////////////////////////////////////////////////////////////////
/// @brief delete authentication data
////////////////////////////////////////////////////////////////////////////////
CookieAuthentication.prototype.endSession = function (req, res) {
this.setCookie(res, "");
};
////////////////////////////////////////////////////////////////////////////////
/// @brief update authentication data
////////////////////////////////////////////////////////////////////////////////
CookieAuthentication.prototype.updateSession = function (req, res, session) {
// update the cookie (expire date)
this.setCookie(res, session._key);
};
////////////////////////////////////////////////////////////////////////////////
/// @brief check whether the authentication handler is responsible
////////////////////////////////////////////////////////////////////////////////
CookieAuthentication.prototype.isResponsible = function () {
return true;
};
////////////////////////////////////////////////////////////////////////////////
/// @brief constructor
////////////////////////////////////////////////////////////////////////////////
function Authentication(applicationContext, sessions, authenticators) {
this._applicationContext = applicationContext;
this._sessions = sessions;
if (!Array.isArray(authenticators)) {
authenticators = [ authenticators ];
}
this._authenticators = authenticators;
}
////////////////////////////////////////////////////////////////////////////////
/// @brief runs the authentication
////////////////////////////////////////////////////////////////////////////////
Authentication.prototype.authenticate = function (req) {
var i,
n = this._authenticators.length,
authenticator,
data,
session;
for (i = 0; i < n; ++i) {
authenticator = this._authenticators[i];
data = authenticator.getAuthenticationData(req);
if (data !== null) {
session = this._sessions.get(data.token);
if (session) {
return session;
}
}
}
return {
errorNum: internal.errors.ERROR_SESSION_UNKNOWN.code
};
};
////////////////////////////////////////////////////////////////////////////////
/// @brief begin a session
////////////////////////////////////////////////////////////////////////////////
Authentication.prototype.beginSession = function (req, res, identifier, data) {
var session = this._sessions.generate(identifier, data),
i,
n = this._authenticators.length,
authenticator;
for (i = 0; i < n; ++i) {
authenticator = this._authenticators[i];
if (authenticator.isResponsible(req) &&
authenticator.beginSession) {
authenticator.beginSession(req, res, session._key, identifier, data);
}
}
return session;
};
////////////////////////////////////////////////////////////////////////////////
/// @brief terminate a session
////////////////////////////////////////////////////////////////////////////////
Authentication.prototype.endSession = function (req, res, token) {
var i,
n = this._authenticators.length,
authenticator;
for (i = 0; i < n; ++i) {
authenticator = this._authenticators[i];
if (authenticator.isResponsible(req) &&
authenticator.endSession) {
authenticator.endSession(req, res);
}
}
this._sessions.terminate(token);
};
////////////////////////////////////////////////////////////////////////////////
/// @brief update a session
////////////////////////////////////////////////////////////////////////////////
Authentication.prototype.updateSession = function (req, res, session) {
var i,
n = this._authenticators.length,
authenticator;
for (i = 0; i < n; ++i) {
authenticator = this._authenticators[i];
if (authenticator.isResponsible(req) &&
authenticator.updateSession) {
authenticator.updateSession(req, res, session);
}
}
session.update();
};
// http://stackoverflow.com/questions/783818/how-do-i-create-a-custom-error-in-javascript
////////////////////////////////////////////////////////////////////////////////
/// @brief constructor
////////////////////////////////////////////////////////////////////////////////
function UserAlreadyExistsError(message) {
this.message = message || "User already exists";
this.statusCode = 400;
}
UserAlreadyExistsError.prototype = new Error();
function UnauthorizedError(message) {
this.message = message || "Unauthorized";
this.statusCode = 401;
}
UnauthorizedError.prototype = new Error();
exports.Users = Users;
exports.Sessions = Sessions;
exports.CookieAuthentication = CookieAuthentication;
exports.Authentication = Authentication;
exports.UnauthorizedError = UnauthorizedError;
exports.UserAlreadyExistsError = UserAlreadyExistsError;
exports.createStandardLoginHandler = createStandardLoginHandler;
exports.createStandardLogoutHandler = createStandardLogoutHandler;
exports.createStandardRegistrationHandler = createStandardRegistrationHandler;
exports.createStandardChangePasswordHandler = createStandardChangePasswordHandler;
exports.createAuthenticationMiddleware = createAuthenticationMiddleware;
exports.createSessionUpdateMiddleware = createSessionUpdateMiddleware;
exports.createAuthObject = createAuthObject;