Skip to content

Commit

Permalink
[REST Auth] API tokens & openhab:users console command (openhab#1735)
Browse files Browse the repository at this point in the history
This adds API tokens as a new credential type. Their format is:
`oh.<name>.<random chars>`

The "oh." prefix is used to tell them apart from a JWT access token,
because they're both used as a Bearer authorization scheme, but there
is no semantic value attached to any of the other parts.

They are stored hashed in the user's profile, and can be listed, added
or removed managed with the new `openhab:users` console command.

Currently the scopes are still not checked, but ultimately they could
be, for instance a scope of e.g. `user admin.items` would mean that the
API token can be used to perform user operations like retrieving info
or sending a command, _and_ managing the items, but nothing else -
even if the user has more permissions because of their role (which
will of course still be checked).

Tokens are normally passed in the Authorization header with the Bearer
scheme, or the X-OPENHAB-TOKEN header, like access tokens.
As a special exception, API tokens can also be used with the Basic
authorization scheme, **even if the allowBasicAuth** option is not
enabled in the "API Security" service, because there's no additional
security risk in allowing that. In that case, the token should be
passed as the username and the password MUST be empty.

In short, this means that all these curl commands will work:
- `curl -H 'Authorization: Bearer <token>' http://localhost:8080/rest/inbox`
- `curl -H 'X-OPENHAB-TOKEN: <token>' http://localhost:8080/rest/inbox`
- `curl -u '<token>[:]' http://localhost:8080/rest/inbox`
- `curl http://<token>@localhost:8080/rest/inbox`

2 REST API operations were adding to the AuthResource, to allow
authenticated users to list their tokens or remove (revoke) one.
Self-service for creating a token or changing the password is more
sensitive so these should be handled with a servlet and pages devoid
of any JavaScript instead of REST API calls, therefore for now they'll
have to be done with the console.

This also fixes regressions introduced with openhab#1713 - the operations
annotated with @RolesAllowed({ Role.USER }) only were not authorized
for administrators anymore.

* Generate a unique salt for each token

Reusing the password salt is bad practice, and changing the
password changes the salt as well which makes all tokens
invalid.

Put the salt in the same field as the hash (concatenated
with a separator) to avoid modifying the JSON DB schema.

* Fix API token authentication, make scope available to security context

The X-OPENHAB-TOKEN header now has priority over the Authorization
header to credentials, if both are set.

* Add self-service pages to change password & create new API token

Signed-off-by: Yannick Schaus <github@schaus.net>
  • Loading branch information
ghys committed Oct 25, 2020
1 parent dd92288 commit 8b52cab
Show file tree
Hide file tree
Showing 21 changed files with 1,472 additions and 250 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.core.io.console.internal.extension;

import java.util.List;
import java.util.Optional;
import java.util.Set;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.auth.ManagedUser;
import org.openhab.core.auth.User;
import org.openhab.core.auth.UserApiToken;
import org.openhab.core.auth.UserRegistry;
import org.openhab.core.io.console.Console;
import org.openhab.core.io.console.extensions.AbstractConsoleCommandExtension;
import org.openhab.core.io.console.extensions.ConsoleCommandExtension;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;

/**
* Console command extension to manage users, sessions and API tokens
*
* @author Yannick Schaus - Initial contribution
*/
@Component(service = ConsoleCommandExtension.class)
@NonNullByDefault
public class UserConsoleCommandExtension extends AbstractConsoleCommandExtension {

private static final String SUBCMD_LIST = "list";
private static final String SUBCMD_ADD = "add";
private static final String SUBCMD_REMOVE = "remove";
private static final String SUBCMD_CHANGEPASSWORD = "changePassword";
private static final String SUBCMD_LISTAPITOKENS = "listApiTokens";
private static final String SUBCMD_ADDAPITOKEN = "addApiToken";
private static final String SUBCMD_RMAPITOKEN = "rmApiToken";
private static final String SUBCMD_CLEARSESSIONS = "clearSessions";

private final UserRegistry userRegistry;

@Activate
public UserConsoleCommandExtension(final @Reference UserRegistry userRegistry) {
super("users", "Access the user registry.");
this.userRegistry = userRegistry;
}

@Override
public List<String> getUsages() {
return List.of(buildCommandUsage(SUBCMD_LIST, "lists all users"),
buildCommandUsage(SUBCMD_ADD + " <userId> <password> <role>",
"adds a new user with the specified role"),
buildCommandUsage(SUBCMD_REMOVE + " <userId>", "removes the given user"),
buildCommandUsage(SUBCMD_CHANGEPASSWORD + " <userId> <newPassword>", "changes the password of a user"),
buildCommandUsage(SUBCMD_LISTAPITOKENS, "lists the API tokens for all users"),
buildCommandUsage(SUBCMD_ADDAPITOKEN + " <userId> <tokenName> <scope>",
"adds a new API token on behalf of the specified user for the specified scope"),
buildCommandUsage(SUBCMD_RMAPITOKEN + " <userId> <tokenName>",
"removes (revokes) the specified API token"),
buildCommandUsage(SUBCMD_CLEARSESSIONS + " <userId>",
"clear the refresh tokens associated with the user (will sign the user out of all sessions)"));
}

@Override
public void execute(String[] args, Console console) {
if (args.length > 0) {
String subCommand = args[0];
switch (subCommand) {
case SUBCMD_LIST:
userRegistry.getAll().forEach(user -> console.println(user.toString()));
break;
case SUBCMD_ADD:
if (args.length == 4) {
User existingUser = userRegistry.get(args[1]);
if (existingUser == null) {
User newUser = userRegistry.register(args[1], args[2], Set.of(args[3]));
console.println(newUser.toString());
console.println("User created.");
} else {
console.println("The user already exists.");
}
} else {
console.printUsage(findUsage(SUBCMD_ADD));
}
break;
case SUBCMD_REMOVE:
if (args.length == 2) {
User user = userRegistry.get(args[1]);
if (user != null) {
userRegistry.remove(user.getName());
console.println("User removed.");
} else {
console.println("User not found.");
}
} else {
console.printUsage(findUsage(SUBCMD_REMOVE));
}
break;
case SUBCMD_CHANGEPASSWORD:
if (args.length == 3) {
User user = userRegistry.get(args[1]);
if (user != null) {
userRegistry.changePassword(user, args[2]);
console.println("Password changed.");
} else {
console.println("User not found.");
}
} else {
console.printUsage(findUsage(SUBCMD_CHANGEPASSWORD));
}
break;
case SUBCMD_LISTAPITOKENS:
userRegistry.getAll().forEach(user -> {
ManagedUser managedUser = (ManagedUser) user;
if (!managedUser.getApiTokens().isEmpty()) {
managedUser.getApiTokens()
.forEach(t -> console.println("user=" + user.toString() + ", " + t.toString()));
}
});
break;
case SUBCMD_ADDAPITOKEN:
if (args.length == 4) {
ManagedUser user = (ManagedUser) userRegistry.get(args[1]);
if (user != null) {
Optional<UserApiToken> userApiToken = user.getApiTokens().stream()
.filter(t -> args[2].equals(t.getName())).findAny();
if (userApiToken.isEmpty()) {
String tokenString = userRegistry.addUserApiToken(user, args[2], args[3]);
console.println(tokenString);
} else {
console.println("Cannot create API token: another one with the same name was found.");
}
} else {
console.println("User not found.");
}
} else {
console.printUsage(findUsage(SUBCMD_ADDAPITOKEN));
}
break;
case SUBCMD_RMAPITOKEN:
if (args.length == 3) {
ManagedUser user = (ManagedUser) userRegistry.get(args[1]);
if (user != null) {
Optional<UserApiToken> userApiToken = user.getApiTokens().stream()
.filter(t -> args[2].equals(t.getName())).findAny();
if (userApiToken.isPresent()) {
userRegistry.removeUserApiToken(user, userApiToken.get());
console.println("API token revoked.");
} else {
console.println("No matching API token found.");
}
} else {
console.println("User not found.");
}
} else {
console.printUsage(findUsage(SUBCMD_RMAPITOKEN));
}
break;
case SUBCMD_CLEARSESSIONS:
if (args.length == 2) {
User user = userRegistry.get(args[1]);
if (user != null) {
userRegistry.clearSessions(user);
console.println("User sessions cleared.");
} else {
console.println("User not found.");
}
} else {
console.printUsage(findUsage(SUBCMD_CLEARSESSIONS));
}
break;
default:
console.println("Unknown command '" + subCommand + "'");
printUsage(console);
break;
}
} else {
printUsage(console);
}
}

private String findUsage(String cmd) {
return getUsages().stream().filter(u -> u.contains(cmd)).findAny().get();
}
}
46 changes: 41 additions & 5 deletions bundles/org.openhab.core.io.http.auth/pages/authorize.html
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,14 @@
margin-bottom: 2rem;
}

form.hide {
display: none;
}

form.show {
display: initial;
}

input {
border: 0;
padding: 10px;
Expand All @@ -54,41 +62,69 @@
margin-bottom: 1rem;
}

input.submit {
.submit {
cursor: pointer;
margin-top: 2rem;
background-color: rgb(33, 150, 243);
text-decoration: none;
color: white;
border-radius: 4px;
font-size: 14px;
font-weight: 600;
min-width: 80px;
padding-top: 10px;
padding-bottom: 10px;
padding-left: 1rem;
padding-right: 1rem;
}

input.submit:hover {
background-color: rgb(72, 168, 245);
}

.result {
display: none;
}

.resultPassword {
display: initial;
}

.resultToken {
display: initial;
}
</style>
</head>
<body>
<img class="logo"
src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAoEAAACNCAMAAAD/0FDYAAAAk1BMVEUAAABHR0eYmJjmShlHR0eYmJjmShlHR0eYmJjmShlHR0eYmJjmShlHR0eYmJjmShlHR0eYmJjmShlHR0eYmJjmShlHR0eYmJjmShlHR0eYmJjmShlHR0eYmJjmShlHR0eYmJjmShlHR0eYmJjmShlHR0eYmJjmShlHR0eYmJjmShlHR0eYmJjmShlHR0eYmJjmShkF3IrcAAAALnRSTlMAEBAQICAgMDAwQEBAUFBQYGBgcHBwgICAj4+Pn5+fr6+vv7+/z8/P39/f7+/vsRLa0QAAGIlJREFUeNrtndeCsjoQgFGxIXYFdbEsuPay7/90hy4JKZNQdP/DXJzihtSPSWYyJIqSSWqaNjJi6WuaUkkl5YjaN7b2Iy1X2zK0WtU/lRRK33R7ejDlxxpVFFZSjPSXHPpiCo121VuV5Cxt6/oQkJOhVn1WSW5Sm54ewmKPqo6rJJ/Fn/WQk6tRLQkrySyaLH8+g8tqMq4km/7bPrJJpQcrybL+sx7Z5WpUHVmJnEyvj1zkVG2YVCLjf/l55CbbaiquRFQMps95a4wSW8E1TZsaNktjXvtVj1YiZIHQFaBt0CbVWn9Jf8yq1GAlcBlR9NlpydNltRFt8+RUbdVVApUleSa1gAz1yS6ca7VJUgnMB2MTl34iMS+qQdzGW1adW4mkDWwLO1RGp8omrkQKwGse/NEY/KkQzDA0WiyA9ZAqlDoWL70qlpwuaj4AyjuUCS7tCkF5SayObH5qQyh1vIb3pyp4en6I1Nboi4z5KN9NNcK23rUyiT+ZwKAMNT8CA70zrUkDaGcMbtFOFYJ/iEAtdN7mTCA4QqWdem6a3bJeVhPx3yEwKkLNm0AYS6k14E8u6qp/rRD8IwRqwn6zXKMD1GtBG2mpLb6fiqbPJDBetl9rBRDIUz01nJNpbl2XMkisCqdPJFB9PWIUQSAHQbvIPTR8MVhFrX4igQlFAVWCop+vwSpchMWKW9lV0OrnEagmB2hUCIEM1dMv2mWCIXitPmH6OAKRtdKpGAIfNLBq18J9diO4Pq7kHQRiDIxECTRSsrV/wPOwXYLTeFQtBT+aQGwddhIlkOLgWV4h668pTFNmlHJKqQiUI7B2lVmqcwlU8PAA4qazKqN/JcSqvIKfS6AhYrcKEYhtddS4c7BRSh9W8/CHEZiOpdPyIhDd7h3x7OBtgX1YO1X28IcSOCLsouVGIKJgLQ4Wp0I3bdulwV4RKEYgKaRdzY3AGnN2N8q0D6a5+KW7ui+NTFVpBJl0M2XSyp5JC9qUIgkcEb+wzY1AZP3FtoFE12athvyLLOUU1Bf7y28sx81cZvBbk83+lcl9vxhKwNydf1+QTHTxtiSqcdl/TVpvI5B8QKSaG4FLRkIri33avR8Fxw61uwXN7uHmNy2Xr5YYfvMjIZfviVjDN3dSJkO4Bp58E9vSfQuBGnkDY5kbgQY9oZplDu66wyCK4FRi7ycYs/nllyJ7XQAdWib3Bbglwz0tkwsskxYR4CCHyRsItClRpbUSCLQyzMFdvxtFEfyRU4ITKn8+g7DJuLVhZXJfwCbPPTOTOf9dWrAy+L3oZROoiQcSCBJoURMiq8BrTQJAYQQ1mZVgiznqnkDomd85mVz4IDe+eDU5cjLpXng5fJVMoI0cQp8kIi8Cf6gJDflVWTceTkEELXFzeMJDBzDwSmPPz4QLcvcIyISpBieADIg9WhiBaFgWopRG+RCo0m2Nk+SiLAmgKIKqcLj05hcidx1cYYZ8M9uiwzLZZAPQ7dFWiQRaKASGABNAAi2qF7gvrQLR8RRD0OJtE0oB6MokO4DstkygNdk3MuZAqEVRBOKRqSKRqhK7cujXH1tZFYiPpxCCqtjnKPvf3+wI6ndwJvS2TH6zZqLDMyiNQAs3BSy4fw5EIPoJksqiXxpAQQQtEfA3v7/ZEezefzPTI5YJcSJuCOSwKIlANWX8qvCFOoRAFECb6pu7ZgJQDMG2gA+SAODxazHU9cnim2BUks2RVrrC9+/FXNf1xRdBxX4rMHzcPIb+rtxicwEhmGrMcTHRaTm0yiHQSHtDLLC3AkAgdhjbiGojG9kAFEPQBjveUxPffpIop/uFV+VOqkUDN2Dvyb2HxmQPsoiPzB2QdFXS+riF+X7mDZazfF8KgYjpaxA0hJaJwLbFCr1WBQMhOJORAIIj6DSMl5Xe/cB9fHtCLl+83Y+Uu5FgVmNu5H0r7WhGq3JvMVXgnUDoN0ufF0OgQXII21BvBYtAVRulr2PtUydhOzOAIggiLx5rGj7y9xsa3zxnnM5jx9toQ1t1STWli9aEuAOMVQV/GRr8zpow5nE76TbmCpBXZCQs4q6BCiPQTgrtSHsM562MHcJajsMRtGDW8Bzkc56w5+HGBeJzxmbqL6ZFfmyBFg0T+h9pXZVMcwdt3mb7TnxKZg2qBAVrgh+aIOiWA9iDYAT7oA5qAJ2OqALbMKdPqsMGU2AtBlqMZiL9c0H/9s2aoUm11Ysn8ERGrQ9UgoI3vNXoG7TbXAAUQPAKseMX0Ky7DHhQjFlO628GxxfmFE2rClraHbL516CmKoLAEY20E8xIFdOAbcYadCoJ4GQjieAWYG0h7DCHnbV+WoB3bNGJuEXNvwu23i80NhmNWdAWkkUQeKLNtiNYxIqQBlRZLhFVEsC0iwuI4AjwjiVXgXdO3AFi7jYomofi6CO7Db9oq0Be8MKeMo9OYPVo0RaCBRA4ortdYEow21W/V9EduS7R4yWHoApYAlygygu3muc0lcSr2pBiB7R4Hkea129DVm7MgOxEs4sm0KanGoF2K0Sv+h3RGLDkAZRF8MTlv8vcJWW5XI6UxR0/iH5PTv0l8i4kO+ROrkkX+LheLIEaw/MMC9LKdM2qJrgM7FJ9/lIIbrmmyILtIWa5exNrOI6zGqa/jgIqEH11dCLdzMcXpRFosxKBgrQkbptuE0vQsgAohyC/+L2ICkTHfUJUjZDviL5JZkSLG79MXREsSLPrBboUWBRKoEbfrIAqQYnavM7FsqAR1lwApRDUuO0D+lBI474hKZQLJJMhSZMmfzzu+XIh6l2oMtbLIpATm2cAgrQeWRC0RQyRLmfbXRxBlWdr6TDnBdF0PpIUKUR7IabzkLIlLCL3Qgi82lw5cQnkxeZBgrSY5weitUjvjJxADiMYgDII8gyhidgkjE7DJMMS9jHvN2H89/IE/ooT2KB4bfKOTOBGaQKCtPjzqNq3TpTvkAW+TOYDKIHgD6d5C1HtpZAmUFFFmtSkG3J8hJjohBdkAW3HvkAC+eHJKj+ME7aS69vEY7ngsYEQAMUR5HXoRnAZiKgqPW1DXGCZ6ITx/82DQP3DCLT4Dj+L67CD2hLY7W42zBIQA1AYQZ4ltBfyxWATqM7gCeyPyYXA4YcSWAN8md7mxieArdnaNqUENagzBgqgKIJG7gQu8iBQyZnAxYcSaEA2frlBWgL+FAtXglAC4QAKIvj5BF7+YQJJwfkcj2EtG4EK/sUckEARAMUQrHTgOwlEDsvoU29J58UniBCoYi5uGIFiAAohWBH4TgKvEp7kWjYCkRHfAgkUBVAEwfwJ3OdBIMcWVqTlowgcyWxmGBkJVFEHJIRAcQAFEOQRuBA7Fwt123UJGyuwTIaE8b9Ao1P/DIEnGQJPGQnETtACECgDIBxBAQI3oCFuEGjTf0XhWbD3RIb/BIFSKpDktRMj0EA83HwC5QAEI8gjUBd1Jg8JD+hCgX2YIiWFn339EwSe5Ag85UigxidQFkAogjwCG6Lqa0PYT9XhIfoph/Sr1LnoFrUyb3wygX3ZKJtRNgI1KoH9XAEEIsgjMLn6AmmeO0Hd6ayDWIjokMJauoKZuFN5+jjoDyIwvzivLDqwxjZysgAIQ9DmRZ9thAKTkQ9CukQCIQbNhagz72KZ+IoUZ/BzCNQe0qLlSCA7MiEbgCAEuR06FIPnQlo36txTjegYT6Rfhj3pkJvPIdCWJ9DO0RZWWMo1K4AQBK/cDr2LjPucGEmv/wopQeSAj4bsy/CqSvKI/48hsP3IIO2c/IEP9iyYHUA+gjX+vuSXgBmBfN3eohDItWgWtO/eL5ADN0i91/o8ApGwLNsWCrfG4xOECFxiSo/+sVoeAHIRBHyq1xKow578TZwuFDDWpR7gtoB/coecvPD9eTpQFb5BhnGSltC+MB4mbdB0az4A8hCcAvZkNuBDE74o7OiQs3Vj4u9UzhAVu4A3u/V5BFriN8jQg7SkY2P6mF98VACAHAQtwJkhiBJkITihHdun/8IRRI+N0enzM7NHNrTSPoRAcRWI+Q9rkgQi8YFXfEE6LQJANoI/kEMhENVGvy/ki7rYS59dv6FNxOhVR9+sdSbjCLgN9RTVDyFwKXNwLjVISzZG2sKetooBkIVgTeIAQcrIYUfwLhQmgbRzMNFrm9LmxhCiSrEbl+af540hn5nKE+pJWpLfiYTo24UDyECwD5sLcEuCoAaxg6SPCodAF4AGj2JSs/dcjvEr544f6JE25M4OP1HsRth9IiObfJjvsngA0wjOCauCPtTE8I/SR8lZYJcgYKtF8g0yd+xK4uE3YLWIX+awwdRk6sZWTI9+BIFyKpB+9DyTwLamaSPD+qGeJNgvAcAUgi3CS1WDulmCgf2e634u3eEi/SHvRAEQ6Omnr6GvTxv6fAO8nieV13EeqeSGTqjK8AN35SRVIPUQGTm39hRZihUMIIbgkeCX/4G717gyV4AEMk/aaPC3XeINQO+cmDszPP+TCDxJXZ+gUE/SkgJwi5ijhQOIIjghWGRLJTcEN0oOBNLdPiIXjLGq8jYCR+CraphKsJ+JwMSB+kYpACYHLtYuJ4GegCO4UXIgkOV33PzmUpW3ESitAlElaGchMGlLt8sBMDFwG8I+D/9COyiCc+baDXip4LElYljRZKJ8IoHyKpB2kpaEBkT234xyAHwNXItgCQMssgZk4InXHCW/fANdjfnNCcIBXfBK9J1/AIE/4NU3Z0/Dlibwh2Z2FgtghOCG4I5m+mJe/hIuPcS7utBvL/H7vwjo8L8l6R4lKX4/gRr3tCywElQlCaSu+YsGMESwRVhTAG+Vxb29uEk65Hq0vdHUL+zFG+iAtwX7bbjo3Kq8iUAbcjI0TAlaUgT+aO8D0Edwk8kt2qJPxZcJYE8lGM0JncFNC1iTBoNBUFXeQ2A2FUhWgkJf2tHLLANAF8E7SQXyLrhGGJyTZsD79xC0qxeNpr4h8XNZtATa0pgQJ/T7RgdV5T0EZlSBaESpJUjg1crtcCJ50UkqUHBB3JpskhTe9wv2J+TkUzv0BeJEPm7mLeHWNIZfSCZuVXRoVd5CoPoQvL4jJVrapwJTflsjp/MBcxIj42ygtPRQANTTQ5ujTFqZ3ip4VRpxWl6Rr4RdfJs1FMDEoeKp1eSZWDWp1qZz0PjCLap0ABEVeCq4MImzsyopWcoGEI2VNSoC//eyLxlAjXckXUXg/0z0cgFEv1cxlIrASvalAmiUqgIrAv+YEiweQO2R2SfwbxBYr0s/2nGc1Ztq7RbtFJDtd3kA1k6PEg3hDyZw9nxKU9R7Pp03Vdst+llAttGH2sUDqGwfGeKD/iUCbxmG8t8jMPwauwQADUqw9v+PwMPzeRZ9pjl4A4GDZhkE+ifXxgD220W1ZvQo1wz5YAI7u11H8IlDxF2JBK6ez14pBOoJAEf0IMKM0r6WbIb8U7bwi7sSCXTKIlD5miT1VDEIYgBulfcT2OxhFmm9F006nV4KgQ7+cBM2eeKFdNLaD8uKlHWKwDqWL7E+dazWcbOwp3vpnmARiBfVwR9P5I81F68ReaIsAkEMwFLmYCaBnbXbnc+bGfSU5+EYuKuz59ldbdXXnqWwDv4ydpyOMvb+9FzHHd/b+Q+v/d40HWcW/LxznOChQegxCdKdE4WMz94gupmOA++GqfQcvx7xEJnnoF4zr+BoOnTcCtwcP1efwLiuIX5+jW9rFASvrOfzMAhbUfdbvHLTDM6Jp5vrc1DJqCjHrZeLnls5N9ODk6iGT6Bf37P5KuWQbGPH6wy/YO+x5o7QtkQ/Uldq+SOIAViGHcwmcPwM5eD3nPvv6Jdx55b8i+kOxi780y0ci3X08HMWZHUIet97OkowIxXi+L/4mXrD0nR/MaMk8Xov/P9DYsZzooycgMCoAuHYDm5YBZFaroJWjKNmxX8I6vpM5B3qPS9lrxf/oZckMGrTOuQ3qm9U9qt2t07Uk6H27hCrSTYV8kYQB9BQ3kzgwBu+XtN7f514GBxz5fbR7eC+0KYTja87du6Lu57NvH69NaOhXQ96/svvglZ3/9UMF+7hwNz8X8be+9/reBphHRTi6wYnJtD95eYV66sG/4f6wc97EAzi630x156aMc1xgIGbxvTVXi8a2XWv4+nFAzJl3ma93urcCVpxe55XfrMO3tN+S/33Ynde9dxkEZGOX6eD02n61Vq7hTaTBD6fO//hIHX95rexZ94irqLaeXrx5tX5nEy76zW99/lW59qq+SKIA7hV3kyg1xlBr5yDUYxUQifWWF4/hQQ+b/5Ae6phF2q2QH3sAvac8P/96TNUhoegkE5UWicsxIkUq6kkivXAu4UQBwPZeaKrfmQdGKTxwQt9O2MF+Y/wPXlNlWaiWa+W+ouHZpziEKnbW49uifh/87rtEPZA8FZ6lYnf5V6k/9dh2w5hhdZRt5kAZ0mOTpkR9JO90ggcx7NI+F+x9lpHCs2dIZ+h9oiGdRb87Rw/7KG18n9fB2Pq9u0g4GiW3PgI/8tTC3UFIzAYi7C0eoRFUC6VwF6U5BzocyfW7LvkzD1DCIybtYpauku+lGF5ZmLaJRE4jueQMLceWq24SYdI1fXitKEPtEn2hqp4ZP8oLwyWeM5t5d0E7uKO6wTDF/fkOB7OQ/CTGWqncHId+73efDnMznGPuv/TCVg8RKqxg+CD4BURGGYV6OLBq7Amg8ADYpquXpovUVdfnY4TBDpxG/CWesbsLCLPTEzlDG9MkHqWSHwO2hQ3yYwJD96uxE6km7QO0FS8A12AUrPfByCVQG/KCCXo1JjA10g78Yg4iRExvRRnbEgOfhcf3NE4hEQegjzDMg4Y5kkCkcFOFsYg0EGSOIHF+mpMiLC3WDvPYqW7wqiK8+msbwmTw0xMkVwC14n97XXcSLSN0dvlrWTDat6wfCMx2CcsyImWuku5r7yfwCciYAJXXqfOEj+8tNDM427m60FPu8zwQsAEmjIEoo2JFNv5GftCXhmnCFwFFnKEhRCBTiKxSSXQidImhEggGkDvq6usGxe1JeCK0AKltYhlghFoxjIW04HjNIHeCtAjs+n918qb45vMQlgErmUIXL8KQpa7TuSyoRPoLW59v6ZTPIGJalKi06z0NU6Z1GD/9F4AqXKOl2hIh/IIPHsrruTW1Cyckc/u+uvgT71n95+3YHGUWC9ihVAJnL1m+KYIgQPqjozn3amzCDxHi1MZAlcJa8aJzS0ygWuKBcxD8LGUNlxV+/GhAKb9AUwCI+9Vx2eq/jKO/fc6XAP1gpF0x2QQjqlDLYRKYAdJAiRwlVCc6X25Gza9YwS+QJdZB46fsU3hZTRgEThDvJVUmRI+d5dzH9cINF+1zwDQ9xV3wARGAxwaFOvYqTKOHhv4ft5m6AoMdR+9ECqB3r+CvDu3FIFnCoGdZ9INiMuBTWAvYmhMJnDMJLB+izsn8p9SCUx4moR8d34w81RYD6rGlQBgW/kU8ZxVvplYHwz4BD53TcXfLPV/8YzMQydcQu1i4zp87vzaYfMKGQeF9IAEejT5OxmYVeGrmJnvPk4R6HuZg4XVIAHMzP9pzJuFwxdsfCMQ6GZ8aCpNOoHBKtLNv74LVSCdwOBd9jPrmWLGq68HVZHxbVvEo5PUjwHQd+qHBuCBPwsHO0yIOzZ4NtzwDSCIt4SjVz3YB/UTOkACX1vJKywSyok2gtME1oPYAK+cG7LWPTjn174whcCV/+jZbyVO4Bgzr9MEBnvMvhso9MvQCQzS3vw6MeOK1B/y+dNQL0ptRMmgpnyQ1OPogjPfEgk39WMHbxQ+cEPGqh7jGfVv0yEXwiAwULVe4ApGYIAzkUClvorDA5pJAuPABAaB9V3YtlWgxJIEKmsugcosdCZGoTYMAhWTG5pAX8EF5x/1AfhtOUf4f4w0Z6azM2dBX/SiCLl6Lwpg6wQ/+bZwfbZyzHHCg9Ax18gPr8e8rHiFKM1eEGL3Stt5hejV/T92EIXmZ7XbmR1KUc2xV46J2g3mylnNmkiBiYLifHqmm6weJXml9N8st92zZCt7r6LD+tYH5m5txtZ4nIRQpFIfmzsHqyZ5MXilHcK1nTLWcppBvb771Fb+qJhv+jBojG7b/t+EMhOHtyIbI/xEJFWbLlmXx1s1pSJQeKE6U/7PYlx5R7Latm0Zxha7hJvIbP8Pd0TJBK6DaLzegRpE9/9Rg/YjJ1nWlIpAqPiWqW8u0jc6/jfSP+XBn93+271QLoHNc2w9d5RK+FMx/wTX/l/vg14QVVCaDEwvfsmsFGDoXMnG4GlUdWElmRmUnot/Kv4qyUVGUjaJpVU9V0ludvFSUBH+TGtVr1WSq7ThEP5M1aq/KikCwumWf3fOdlThV0mhFFo2dbduWdFXSTnLQm1qLG1PTj/+v4z0TnEllTDlP+Wgb6SN+7dxAAAAAElFTkSuQmCC" />
<form method="POST" action="/auth">
<div class="message">{message}</div>
<form class="{formClass}" method="POST" action="{formAction}">
{form_fields}
<div class="message">{message}</div>
<div>
<input class="field" autocomplete="off" type="text" placeholder="User name" name="username" required autofocus />
</div>
<div>
<input class="field" type="password" placeholder="Password" name="password" required />
</div>
<div>
<input class="field" type="{repeatPasswordFieldType}" placeholder="Confirm Password" name="password_repeat" />
<input class="field" type="{newPasswordFieldType}" placeholder="New Password" name="new_password" />
</div>
<div>
<input class="field" type="{repeatPasswordFieldType}" placeholder="Confirm New Password" name="password_repeat" />
</div>
<div>
<input class="field" type="{tokenNameFieldType}" placeholder="Token Name" name="token_name" />
</div>
<div>
<input class="field" type="{tokenScopeFieldType}" placeholder="Token Scope (optional)" name="token_scope" />
</div>
<div>
<input class="submit" type="Submit" value="{buttonLabel}" />
</div>
</form>
<div class="result{resultClass}">
<a class="submit" type="button" href="/">Return Home</a>
</div>
</body>
</html>
Loading

0 comments on commit 8b52cab

Please sign in to comment.