Using the default "intro page" for login and accessing the user-defined web pages can be difficult and not user-friendly for the client. This was my case for a project that determined me to create a custom login page.
This article will describe some mechanics behind the login system in the web server of S7-1200 Simatic PLCs and try to create a custom login page with style. My research began with application note 68011496 from Siemens, which you should also read. For me this article described a method too complicated and hard to adapt in any custom project. Before continuing I recommend reading the S7-1200 system manual (chapter 12) first.
My custom login page was tested with: S7-1214C fw 4.2, Tia Portal Prof. V15, jQuery v3.3.1, Boostrap v4.1.3, FontAwesome v4.7 and FireFox v64.0
Backtracking the login mechanism
The above application note, regarding the login, is based on the default page - "Intro page". So for it to work you need to load the "Intro page" in background, hide or use some css to style the login section of the page and display it on your custom page. Troublesome and complicated. Now lets do some backtracking.
I don't know if it's a bug in this firmware or not but if the project is compiled with entry page selection for user-page "UP1", the default intro pages can not be direct accessed anymore. If you type the address directly in the browser you will receive error 404. To still get access to the "intro page" you have to add a link to it in your own page. <a href="/Portal/Portal.mwsl">Portal Page</a>
This happens because the page needs to be accessed with a referer in header. Of course you can overwrite the referer with a plug-in in your browser and not add the link 😜. Another method is to use the basic version of "Intro page" [IP_ADDRESS]/basic/Portal.mwsl - I guess they forgot to add referer check in the basic version.
I first started by generating an invalid login from the basic login page ([IP_ADDRESS]/basic/Portal.mwsl?PriNav=Login) and observed that the login form POSTS to "[IP_ADDRESS]/FormLogin" three parameters in header: "Login", "Password" and "Redirection". Trying to POST or GET without any parameters will generate an Invalid request - 400 error.
The response from the FormLogin is actually a redirection header 302 to the Redirection parameter. In case of login failure it redirects to the page with a parameter "InvalidLogin" set to true. We will use this to get a nice error feedback to our custom login page.
In case of a valid login the redirection is done to the specified page and a cookie is created for your connection. The cookie name is: "siemens_ad_session" and contains a value of 40 characters - I guess a SHA-1 hash. From now on, the browser will add this cookie to every http request so the web server will receive its value and determine your connection.
The S7-1200 web server can handle maximum 30 active connections and maximum 7 concurrent logins. Modern browsers use even 6 connections, kept alive, at a time per tab so its best to limit the webserver project to 4-5 users with one tab (window) per user. The authentification mechanism requires cookies to be enabled in the browser for season control. For the custom login it is also important to enable javascript in the browser.
Logout
It is important to logout before closing the browser window because of the limitation of concurrent logins and also for security reasons.
Logging out is done by posting an empty parameter "LOGOUT" to "FormLogin" or just by accessing a link <a href="/FormLogin?LOGOUT">LogOut</a>
.
You can also use the parametre "Redirection" to redirect to your custom login page.
Afer the logout, "FormLogin" will modify the cookie to expire in year 1999 so the browser will obviously delete it. Until this the cookie will remain in browser storage. In some logout methods I've seen parameter "Cookie" passed with the value of the "siemens_ad_session" cookie.
Using the cookie value/presence to determine if one's logged is not a good method because the active user can be deleted after 30 minutes from the server table but not in browser storage. If the browser sends an old cookie, the server will ignore it and will not issue a delete or expiration for this cookie. The only method is to rewrite the value with a new login.
Conclusion
- User must send a POST to "/FormLogin" with all parameters: "Login", "Password" and "Redirection" and with referer from same IP address
- If login is invalid, the user will be redirected to the specified page with parameter "InvalidLogin" set to true. If login is valid, the user will be redirected to the specified page and cookie "siemens_ad_session" will be created.
- The logout can be done by POST-ing parameter "LOGOUT" with no value to "/FormLogin"
Remember "HTTPS" and "HTTP" are considered different domains for the referer.
Example of custom login HTML file
PLC Configuration
Open Device configuration and activate the Web server on all modules, disable automatic update and if you like you can disable HTTPS access restriction to allow unsecured connection.
Add the users names, access levels and passwords. In my case I added user name "admin" with full access (all access levels selected).
Open OB1 and add WWW function. Enter 333 as CTRL_DB and any available word as RET_VAL.
Create a new folder for the user-defined pages. I usually create it in Tia Portal project folder under the name "HTMLFiles". For good file management store JavaScript, CSS, fonts and image files in subfolders like "js", "css", "fonts", respectively "img". You can download the min version of Bootstrap, Font Awesome and jQuery and copy them in those subfolders if you want your application to work without Internet access.
HTML coding
Create the login.html file and add the login form. Here is a quick example:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="stylesheet" href="css/bootstrap.min.css">
<link rel="stylesheet" href="css/font-awesome.min.css">
<link rel="stylesheet" href="css/login.css">
<script src="js/jquery-3.3.1.min.js"></script>
<script src="js/login.js"></script>
<title>Custom Login Page</title>
</head>
<body>
<div class="container">
<div class="card border-secondary text-center mx-auto" style="max-width: 380px;">
<img id="logo-img" class="card-img-top py-3" src="img/logo.png">
<div class="card-body">
<h5 class="card-title">Authentification</h5>
<h6 id="idLogFailed" class="card-subtitle"></h6>
<form enctype="application/x-www-form-urlencoded" action="/FormLogin" method="POST" autocomplete="off">
<input type="hidden" id="idRedirection" name="Redirection" value="">
<div class="form-group">
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-user" aria-hidden="true"></i></span>
</div>
<input type="text" name="Login" class="form-control" placeholder="User name" required>
</div>
</div>
<div class="form-group">
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-lock" aria-hidden="true"></i></span>
</div>
<input type="password" name="Password" class="form-control" placeholder="Password" required>
</div>
</div>
<button type="submit" class="btn btn-primary">Login</button>
</form>
<a href="/Portal/Portal.mwsl" class="btn btn-secondary mt-4">PLC Portal</a>
</div>
</div>
</div>
<script>
$(function() {
if (getUrlParameter('InvalidLogin')=='true'){
$('#idLogFailed').html('<strong>Login failed</strong>').addClass('text-danger');
}
//Redirect to different pages based on username if needed
switch( getUserName() ) {
case "Everybody":
;
break;
case "admin":
location.href='/awp/CustomLogin/app.html';
break;
case "OtherUsers":
;
break;
default:
;
}
});
</script>
</body>
</html>
Some styling is done in css/login.css.
html,
body {
height: 100%;
}
body {
display: -ms-flexbox;
display: -webkit-box;
display: flex;
-ms-flex-align: center;
-ms-flex-pack: center;
-webkit-box-align: center;
align-items: center;
-webkit-box-pack: center;
justify-content: center;
padding-top: 40px;
padding-bottom: 40px;
background-image: linear-gradient(rgb(104, 145, 162),rgb(75, 84, 214));
}
There are 2 functions: one for calling an Ajax to get the user name and one to read the URL parameter. The functions are stored in js/login.js. The code for getting the parameters value I've found it here (not mine).
var getUrlParameter = function getUrlParameter(sParam) {
var sPageURL = window.location.search.substring(1),
sURLVariables = sPageURL.split('&'),
sParameterName,
i;
for (i = 0; i < sURLVariables.length; i++) {
sParameterName = sURLVariables[i].split('=');
if (sParameterName[0] === sParam) {
return sParameterName[1] === undefined ? true : decodeURIComponent(sParameterName[1]);
}
}
};
function getUserName() {
var userName;
$.ajax({
url : "/basic/Portal.mwsl",
type : "GET",
dataType: 'html',
cache: false,
async: false,
success : function(data) {
var userNameAjax = $(data).find('#login div').text().replace(' ','').replace('(','').replace(')','');
if(userNameAjax == '') {
userName = 'Everybody';
}
else userName = userNameAjax;
},
error: function(xhr) {
// alert("An error occured: " + xhr.status + " " + xhr.statusText);
userName = 'ERROR';
}
});
return userName;
}
After a successful login the page will redirect based on user name to a specified custom page that holds your web application. In this example it will redirect to app.html for user "admin". Here is the code for a user check inside the application page.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<script src="js/jquery-3.3.1.min.js"></script>
<script src="js/login.js"></script>
<title>Logged Application</title>
<script>
if (getUserName() != "admin") {
location.href='/awp/CustomLogin/login.html';
}
</script>
</head>
<body>
<p>This is the page after login</p>
<p><a href="/FormLogin?LOGOUT">LogOut</a></p>
</body>
</html>
Getting The Username After Logging-in
So far I've found just two methods. You can get the user name and ID from the SERVER variables or a custom hack inspired from the Siemens application note. The first method is not secure; it is suited just for monitoring application because you need to set the minimum access level of every user (even to "Everybody") to: "open user-defined web pages" and "write in user-defined pages". Doing this, one can modify PLC variables without being logged by just sending POST headers to the page. This is why is not recommended except for monitoring applications where no variables need modifying.
Another way, as you can interpret from the script file "login.js", function "getUserName", you read the PLC "intro page" and get the actual user name text with jQuery from the loaded HTML code. This ajax request is synchronic to block the execution of other function until the user name is read. To minimize bandwidth consumption, I used the basic version of the PLC intro page. All the other code is to strip the the string to a usefull information.
After getting the user name you can do redirects in the <head> of the application page before the loading of the html file so it redirects to the login page if the user doesn't have wrights to display the page.
Upload changes to PLC
After creating or modifying any file in the HTML folder, you need to generate the new DBs and upload them to the PLC before they can take effect.
Download the Example Project
You can download the whole Tia Portal v15 example project with the above example from: here.