diff --git a/lib/NGCP/Panel/Controller/Login.pm b/lib/NGCP/Panel/Controller/Login.pm
index d6d7c8f7ac..43ad80bfcc 100644
--- a/lib/NGCP/Panel/Controller/Login.pm
+++ b/lib/NGCP/Panel/Controller/Login.pm
@@ -10,6 +10,8 @@ use UUID;
 use NGCP::Panel::Form;
 
 use NGCP::Panel::Utils::Auth;
+use NGCP::Panel::Utils::Form;
+use NGCP::Panel::Utils::Subscriber;
 
 sub login_index :Path Form {
     my ( $self, $c, $realm ) = @_;
@@ -80,7 +82,7 @@ sub login_index :Path Form {
 
             $c->session->{user_tz} = undef;  # reset to reload from db
             $c->session->{user_tz_name} = undef;  # reset to reload from db
-            my $target = $c->session->{'target'} || '/';
+            my $target = $c->session->{'target'} || '/dashboard';
             delete $c->session->{target};
             $target =~ s!^https?://[^/]+/!/!;
             $c->log->debug("Login::index auth ok, redirecting to $target");
@@ -273,6 +275,138 @@ sub recover_password :Chained('/') :PathPart('recoverpassword') :Args(0) {
     );
 }
 
+sub change_password :Chained('/') :PathPart('changepassword') Args(0) {
+    my ($self, $c) = @_;
+
+    my $realm = $c->req->env->{NGCP_REALM} // 'admin';
+
+    $c->user->logout if $c->user;
+
+    my $posted = ($c->req->method eq 'POST');
+    my $form = NGCP::Panel::Form::get("NGCP::Panel::Form::PasswordChange", $c);
+    $form->process(
+        posted => $posted,
+        params => $c->request->params,
+        item => { username => $c->stash->{username} },
+    );
+
+    if($posted && $form->validated) {
+        $c->log->debug("login form validated");
+        my $user = $form->field('username')->value;
+        my $pass = $form->field('password')->value;
+        my $new_pass = $form->field('new_password')->value;
+        my $new_pass2 = $form->field('new_password2')->value;
+        $c->log->debug("Password change user=$user, realm=$realm");
+        my $res;
+        if($realm eq 'admin') {
+            $res = NGCP::Panel::Utils::Auth::perform_auth($c, $user, $pass, 'admin', 'admin_bcrypt');
+        } elsif($realm eq 'subscriber') {
+            my ($u, $d, $t) = split /\@/, $user;
+            if(defined $t) {
+                # in case username is an email address
+                $u = $u . '@' . $d;
+                $d = $t;
+            }
+            unless(defined $d) {
+                $d = $c->req->uri->host;
+            }
+            $res = NGCP::Panel::Utils::Auth::perform_subscriber_auth($c, $u, $d, $pass);
+        }
+
+        if($res) {
+            # auth ok
+
+            if ($pass eq $new_pass) {
+                $form->field('new_password')->add_error($c->loc('Password must not be equal to the old password'));
+            } elsif ($new_pass ne $new_pass2) {
+                $form->field('new_password2')->add_error($c->loc('New password fields do not match'));
+            } else {
+                NGCP::Panel::Utils::Form::validate_password(
+                    c => $c, field => $form->field('new_password'), admin => $realm eq 'admin', password_change => 1
+                );
+            }
+
+            if (!$form->has_errors) {
+                if ($realm eq 'admin') {
+                    use Crypt::JWT qw/encode_jwt/;
+
+                    $c->user->update({
+                        saltedpass => NGCP::Panel::Utils::Auth::generate_salted_hash($new_pass)
+                    });
+                    NGCP::Panel::Utils::Admin::insert_password_journal(
+                        $c, $c->user, $new_pass
+                    );
+
+                    my $key = $c->config->{'Plugin::Authentication'}{api_admin_jwt}{credential}{jwt_key};
+                    my $relative_exp = $c->config->{'Plugin::Authentication'}{api_admin_jwt}{credential}{relative_exp};
+                    my $alg = $c->config->{'Plugin::Authentication'}{api_admin_jwt}{credential}{alg};
+
+                    unless ($key) {
+                        NGCP::Panel::Utils::Message::error(
+                            c    => $c,
+                            desc => $c->loc('No JWT key has been configured.'),
+                        );
+                    }
+
+                    my $jwt_data = {
+                        id => $c->user->id,
+                        username => $c->user->login,
+                    };
+                    my $token = encode_jwt(
+                        payload => $jwt_data,
+                        key => $key,
+                        alg => $alg,
+                        $relative_exp ? (relative_exp => $relative_exp) : (),
+                    );
+
+                    $c->session->{aui_adminId} = $c->user->id;
+                    $c->session->{aui_jwt} = $token;
+                } else {
+                    $c->user->provisioning_voip_subscriber->update({
+                        webpassword =>
+                            $NGCP::Panel::Utils::Auth::ENCRYPT_SUBSCRIBER_WEBPASSWORDS
+                                ? NGCP::Panel::Utils::Auth::generate_salted_hash($new_pass)
+                                : $new_pass
+                    });
+                    NGCP::Panel::Utils::Subscriber::insert_webpassword_journal(
+                        $c, $c->user->provisioning_voip_subscriber, $new_pass
+                    );
+
+                }
+                $c->log->debug("Password successfully changed for user=$user, realm=$realm");
+                $c->session->{user_tz} = undef;  # reset to reload from db
+                $c->session->{user_tz_name} = undef;  # reset to reload from db
+                my $target = $c->session->{'target'} || '/dashboard';
+                delete $c->session->{target};
+                $target =~ s!^https?://[^/]+/!/!;
+                $c->log->debug("Login::index auth ok, redirecting to $target");
+                NGCP::Panel::Utils::Message::info(
+                    c    => $c,
+                    desc => $c->loc('Password successfully changed'),
+                );
+                $c->response->redirect($target);
+            }
+        } else {
+            $c->log->warn("invalid http login from '".$c->qs($c->req->address)."'");
+            $c->log->debug("Login::index auth failed");
+            $form->add_form_error($c->loc('Invalid username/password'));
+        }
+    } else {
+        # initial get
+    }
+
+    if ($form->has_errors) {
+        my $request_ip = $c->request->address;
+        $c->log->error("NGCP Panel Password Change failed realm=$realm ip=" . $c->qs($request_ip));
+    }
+
+    $c->stash(
+        form => $form,
+        realm => $realm,
+        template => 'login/change_password.tt',
+    );
+}
+
 1;
 
 __END__
diff --git a/lib/NGCP/Panel/Controller/Root.pm b/lib/NGCP/Panel/Controller/Root.pm
index a2bfe1380f..1db803fc2f 100644
--- a/lib/NGCP/Panel/Controller/Root.pm
+++ b/lib/NGCP/Panel/Controller/Root.pm
@@ -99,6 +99,7 @@ sub auto :Private {
         or $c->req->uri->path =~ m|^/recoverwebpassword/?$|
         or $c->req->uri->path =~ m|^/resetwebpassword/?$|
         or $c->req->uri->path =~ m|^/resetpassword/?$|
+        or $c->req->uri->path =~ m|^/changepassword/?$|
         or $c->req->uri->path =~ m|^/api/passwordreset/?$|
         or $c->req->uri->path =~ m|^/api/passwordrecovery/?$|
         or $c->req->uri->path =~ m|^/internalsms/receive/?$|
@@ -477,6 +478,13 @@ sub check_user_access {
         return;
     }
 
+    # redirect to password change page if password is expired
+    if (! NGCP::Panel::Utils::Auth::check_max_age($c)) {
+        $c->session(target => $c->request->uri);
+        $c->response->redirect($c->uri_for('/changepassword'));
+        return;
+    }
+
     return 1;
 }
 
diff --git a/lib/NGCP/Panel/Form/PasswordChange.pm b/lib/NGCP/Panel/Form/PasswordChange.pm
new file mode 100644
index 0000000000..02ef196326
--- /dev/null
+++ b/lib/NGCP/Panel/Form/PasswordChange.pm
@@ -0,0 +1,52 @@
+package NGCP::Panel::Form::PasswordChange;
+
+use HTML::FormHandler::Moose;
+extends 'HTML::FormHandler';
+
+use HTML::FormHandler::Widget::Block::Bootstrap;
+
+has '+widget_wrapper' => ( default => 'Bootstrap' );
+
+sub build_form_tags {{ error_class => 'label label-secondary'}}
+
+has_field 'username' => (
+    type => 'Text',
+    required => 1,
+    element_attr => { placeholder => 'Username' },
+    element_class => [qw/login username-field/],
+    wrapper_class => [qw/login-fields field control-group/],
+);
+
+has_field 'password' => (
+    type => 'Password',
+    required => 1,
+    element_attr => { placeholder => 'Password' },
+    element_class => [qw/login password-field/],
+    wrapper_class => [qw/login-fields field control-group/],
+);
+
+has_field 'new_password' => (
+    type => 'Password',
+    required => 1,
+    element_attr => { placeholder => 'New Password' },
+    element_class => [qw/login password-field/],
+    wrapper_class => [qw/login-fields field control-group/],
+);
+
+has_field 'new_password2' => (
+    type => 'Password',
+    required => 1,
+    element_attr => { placeholder => 'New Password Again' },
+    element_class => [qw/login password-field/],
+    wrapper_class => [qw/login-fields field control-group/],
+);
+
+has_field 'submit' => (
+    type => 'Submit',
+    value => 'Submit',
+    label => '',
+    element_class => [qw/button btn btn-primary btn-large/],
+);
+
+1;
+# vim: set tabstop=4 expandtab:
diff --git a/lib/NGCP/Panel/Role/API.pm b/lib/NGCP/Panel/Role/API.pm
index b6a355d220..de78d0456d 100644
--- a/lib/NGCP/Panel/Role/API.pm
+++ b/lib/NGCP/Panel/Role/API.pm
@@ -16,6 +16,7 @@ use HTTP::Status qw(:constants);
 use Scalar::Util qw/blessed/;
 use DateTime::Format::HTTP qw();
 use DateTime::Format::RFC3339 qw();
+use DateTime::Format::Strptime;
 use Types::Standard qw(InstanceOf);
 use Regexp::Common qw(delimited); # $RE{delimited}
 use Encode qw( encode_utf8 );
@@ -24,6 +25,7 @@ use HTTP::Headers::Util qw(split_header_words);
 use Data::Compare;
 use Data::HAL qw();
 use Data::HAL::Link qw();
+use NGCP::Panel::Utils::Auth qw();
 use NGCP::Panel::Utils::ValidateJSON qw();
 use NGCP::Panel::Utils::Journal qw();
 use List::Util qw(any all);
@@ -1772,6 +1774,25 @@ sub check_licenses {
 sub validate_request {
     my ($self, $c) = @_;
 
+    if (! $self->check_allowed_ngcp_types($c)) {
+        $self->error($c, HTTP_NOT_FOUND, "Path not found");
+        return;
+    }
+
+    if (! $self->check_licenses($c)) {
+        $self->error($c, HTTP_FORBIDDEN, "Invalid license");
+        return;
+    }
+
+    if (! NGCP::Panel::Utils::Auth::check_max_age($c)) {
+        if ($c->req->method =~ /^(PUT|PATCH)$/ && $c->req->path =~ /^api\/(admins|subscribers)\//) {
+            $c->stash->{validate_password_change} = 1;
+        } else {
+            $self->error($c, HTTP_FORBIDDEN, "Password expired");
+            return;
+        }
+    }
+
     my $page = $c->request->params->{page} // 1;
     my $rows = $c->request->params->{rows} // 10;
 
diff --git a/lib/NGCP/Panel/Role/API/Admins.pm b/lib/NGCP/Panel/Role/API/Admins.pm
index 0b15fb74d8..7a4bc3549c 100644
--- a/lib/NGCP/Panel/Role/API/Admins.pm
+++ b/lib/NGCP/Panel/Role/API/Admins.pm
@@ -155,6 +155,13 @@ sub update_item {
         resource => $resource,
     );
 
+    if ($c->stash->{validate_password_change}) {
+        if (!$resource->{password} || $resource->{password} eq $old_resource->{saltedpass}) {
+            $self->error($c, HTTP_FORBIDDEN, "Password expired");
+            return;
+        }
+    }
+
     if ($item->id == $c->user->id) {
         # user cannot modify the following own permissions for security reasons
         my $own_forbidden = 0;
diff --git a/lib/NGCP/Panel/Role/API/Subscribers.pm b/lib/NGCP/Panel/Role/API/Subscribers.pm
index f1102b663c..ca0bc4723e 100644
--- a/lib/NGCP/Panel/Role/API/Subscribers.pm
+++ b/lib/NGCP/Panel/Role/API/Subscribers.pm
@@ -407,6 +407,13 @@ sub update_item {
     my $groupmembers = $full_resource->{groupmembers};
     my $prov_subscriber = $subscriber->provisioning_voip_subscriber;
 
+    if ($c->stash->{validate_password_change}) {
+        if (!$resource->{webpassword}) {
+            $self->error($c, HTTP_FORBIDDEN, "Password expired");
+            return;
+        }
+    }
+
     $self->process_form_resource($c, $item, $full_resource, $resource, $form);
 
     if($subscriber->provisioning_voip_subscriber->is_pbx_pilot && !is_true($resource->{is_pbx_pilot})) {
diff --git a/lib/NGCP/Panel/Role/Entities.pm b/lib/NGCP/Panel/Role/Entities.pm
index 56cfa1f871..ae98833eb1 100644
--- a/lib/NGCP/Panel/Role/Entities.pm
+++ b/lib/NGCP/Panel/Role/Entities.pm
@@ -20,14 +20,7 @@ sub auto :Private {
     if ($self->get_config('log_request')) {
         $self->log_request($c);
     }
-    if (! $self->check_allowed_ngcp_types($c)) {
-        $self->error($c, HTTP_NOT_FOUND, "Path not found");
-        return;
-    }
-    if (! $self->check_licenses($c)) {
-        $self->error($c, HTTP_FORBIDDEN, "Invalid license");
-        return;
-    }
+
     return $self->validate_request($c);
 }
 
diff --git a/lib/NGCP/Panel/Role/EntitiesItem.pm b/lib/NGCP/Panel/Role/EntitiesItem.pm
index 0602da1eb7..93875e0969 100644
--- a/lib/NGCP/Panel/Role/EntitiesItem.pm
+++ b/lib/NGCP/Panel/Role/EntitiesItem.pm
@@ -25,14 +25,7 @@ sub auto :Private {
     if ($self->get_config('log_request')) {
         $self->log_request($c);
     }
-    if (! $self->check_allowed_ngcp_types($c)) {
-        $self->error($c, HTTP_NOT_FOUND, "Path not found");
-        return;
-    }
-    if (! $self->check_licenses($c)) {
-        $self->error($c, HTTP_FORBIDDEN, "Invalid license");
-        return;
-    }
+
     return $self->validate_request($c);
 }
 
diff --git a/lib/NGCP/Panel/Utils/Auth.pm b/lib/NGCP/Panel/Utils/Auth.pm
index f15f6ea1b1..e046072d3c 100644
--- a/lib/NGCP/Panel/Utils/Auth.pm
+++ b/lib/NGCP/Panel/Utils/Auth.pm
@@ -340,7 +340,7 @@ sub check_openvpn_availability {
     my $openvpn_service = $config->{service};
     my $output = cmd($c, {no_debug_output =>1 }, $systemctl_cmd, 'list-unit-files', 'openvpn.service');
     #$c->log->debug( $output );
-    if ($output =~/^openvpn.service/m) {
+    if ($output =~ /^openvpn.service/m) {
         $res = 1;
     }
     return $res;
@@ -652,4 +652,32 @@ sub ban_user {
     return;
 }
 
+sub check_max_age {
+    my $c = shift;
+
+    my $pass_last_modify_time;
+    my $strp = DateTime::Format::Strptime->new(
+        pattern => '%Y-%m-%dT%H:%M:%S',
+        time_zone => 'local',
+    );
+    if ($c->user->roles eq 'subscriber' || $c->user->roles eq 'subscriberadmin') {
+        my $webpass_last_modify = $c->user->webpassword_modify_timestamp;
+        my $dt = $strp->parse_datetime($webpass_last_modify // '');
+        $pass_last_modify_time = $dt->epoch if $dt;
+    } else {
+        my $saltedpass_last_modify = $c->user->saltedpass_modify_timestamp;
+        my $dt = $strp->parse_datetime($saltedpass_last_modify // '');
+        $pass_last_modify_time = $dt->epoch if $dt;
+    }
+    if ($pass_last_modify_time) {
+        my $max_age = $c->config->{security}{password}{web_max_age_days} // 0;
+        if (defined $max_age && $max_age > 0) {
+            if ($pass_last_modify_time < (time()-$max_age*24*60*60)) {
+                return;
+            }
+        }
+    }
+    return 1;
+}
+
 1;
diff --git a/lib/NGCP/Panel/Utils/Form.pm b/lib/NGCP/Panel/Utils/Form.pm
index e110768656..7bfe536585 100644
--- a/lib/NGCP/Panel/Utils/Form.pm
+++ b/lib/NGCP/Panel/Utils/Form.pm
@@ -8,6 +8,7 @@ sub validate_password {
     my %params = @_;
     my $c = $params{c};
     my $field = $params{field};
+    my $pass_change = $params{password_change};
     my $pw = $c->config->{security}{password};
     my $utf8 = $params{utf8} // 1;
     my $pass = $field->value;
@@ -25,6 +26,8 @@ sub validate_password {
         $is_sip_password = 1;
     } elsif ($field->name eq 'webpassword') {
         $is_web_password = 1;
+    } elsif ($field->name eq 'new_password') {
+        $is_web_password = 1;
     }
 
     if ($is_sip_password) {
@@ -88,6 +91,9 @@ sub validate_password {
         my $prov_sub = $c->stash->{subscriber}
                         ? $c->stash->{subscriber}->provisioning_voip_subscriber
                         : undef;
+        if ($pass_change && !$prov_sub) {
+            $prov_sub = $c->user->provisioning_voip_subscriber;
+        }
         if ($field->form->field('username')) {
             $user = $field->form->field('username')->value;
         } elsif ($prov_sub) {
@@ -105,6 +111,9 @@ sub validate_password {
         my $prov_sub = $c->stash->{subscriber}
                 ? $c->stash->{subscriber}->provisioning_voip_subscriber
                 : undef;
+        if ($pass_change && !$prov_sub) {
+            $prov_sub = $c->user->provisioning_voip_subscriber;
+        }
         if ($field->form->field('webusername')) {
             $user = $field->form->field('webusername')->value;
         } elsif($prov_sub) {
@@ -120,6 +129,9 @@ sub validate_password {
     } elsif ($is_admin_password) {
         my $user;
         my $admin = $c->stash->{administrator} // undef;
+        if ($pass_change && !$admin) {
+            $admin = $c->user;
+        }
         if ($field->form->field('login')) {
             $user = $field->form->field('login')->value;
         } elsif($admin) {
diff --git a/share/layout/wrapper.tt b/share/layout/wrapper.tt
index 444a9974ef..87915948ed 100644
--- a/share/layout/wrapper.tt
+++ b/share/layout/wrapper.tt
@@ -1,7 +1,7 @@
 [%
     IF template.name.match('^api|(\.html$|\.css$|\.js$|\.txt$)');
         content;
-    ELSIF template.name.match('^login\/login\.tt$');
+    ELSIF template.name.match('^login\/(login|change_password)\.tt$');
        content WRAPPER html.tt;
     ELSE;
        content WRAPPER html.tt + body.tt;
diff --git a/share/templates/login/change_password.tt b/share/templates/login/change_password.tt
new file mode 100644
index 0000000000..502f9963b9
--- /dev/null
+++ b/share/templates/login/change_password.tt
@@ -0,0 +1,69 @@
+<body class="login"  id="change_password_page_v1">
+    <div class="account-container login stacked">
+
+[% IF messages -%]
+<div>
+    [% FOREACH m IN messages -%]
+        <div class="alert alert-[% m.type %]">[% m.text %]</div>
+    [% END -%]
+</div>
+[% END -%]
+
+
+        <div class="content clearfix">
+            <h1>[% c.loc('Password Change') %]</h1>
+            <p>[% c.loc('Change password using your [_1] credentials:', realm.ucfirst) %]</p>
+            [% form.render %]
+        </div>
+    </div>
+    <div class="login-extra">
+        [% IF realm == 'subscriber' && c.config.security.password_allow_recovery -%]
+        [% c.loc('Forgot your password?') %] <a href="[% c.uri_for_action('/subscriber/reset_webpassword_nosubscriber') %]">[% c.loc('Reset Password') %]</a>.
+        [% ELSIF realm == 'admin' -%]
+        [% c.loc('Forgot your password?') %] <a href="[% c.uri_for_action('/login/reset_password') %]">[% c.loc('Reset Password') %]</a>.
+        [% END -%]
+        <br/>
+    </div>
+
+<div class="login-footer">
+    [% IF c.config.general.ui_enable && realm != 'subscriber' -%]
+        <div>
+            <b><a href="[% c.uri_for('/') -%]v2/#/login/admin" style="padding-right: 20px">[% c.loc('GO TO NEW ADMIN PANEL') -%]</a></b>
+        </div>
+    [% END -%]
+</div class="footer">
+
+    <script src="/js/libs/jquery-1.7.2.min.js"></script>
+    <script src="/js/libs/jquery-ui-1.10.3.custom.min.js"></script>
+    <script src="/js/libs/jquery.ui.touch-punch.min.js"></script>
+    <script src="/js/libs/bootstrap/bootstrap.min.js"></script>
+    <script src="/js/Theme.js"></script>
+    <script src="/js/signin.js"></script>
+
+    <script>
+        $(function () {
+            Theme.init();
+            Object.keys(localStorage).forEach((key)=>{
+                if(!key.startsWith('DataTables_') && !key.startsWith('aui_')){
+                    localStorage.removeItem(key);
+                }
+            })
+            localStorage.removeItem('aui_jwt');
+            localStorage.removeItem('aui_adminId');
+        });
+    </script>
+    <style>
+        .login-footer {
+          box-sizing: border-box;
+          position: fixed;
+          left: 0;
+          bottom: 0;
+          width: 100%;
+          color: white;
+          text-align: right;
+          padding-bottom: 30px;
+          padding-right: 50px;
+        }
+    </style>
+</body>
+[% # vim: set tabstop=4 syntax=html expandtab: -%]