Skip to content
Snippets Groups Projects
Commit 2feeed63 authored by Mart van Santen's avatar Mart van Santen
Browse files

Merge branch '76-use-kratos-as-identity-manager' of...

Merge branch '76-use-kratos-as-identity-manager' of open.greenhost.net:/openappstack/single-sign-on into 76-use-kratos-as-identity-manager
parents cfc0d6a9 efb6ca4e
No related branches found
No related tags found
1 merge request!50Implemente basic flask + database APIs
Showing
with 987 additions and 20 deletions
......@@ -10,10 +10,17 @@ then
fi
admin=`ssh $host -lroot kubectl get service -n stackspin|grep single-sign-on-kratos-admin | awk '{print $3'}`
public=`ssh $host -lroot kubectl get service -n stackspin|grep single-sign-on-kratos-public | awk '{print $3}'`
admin=`ssh $host -lroot kubectl get service -n oas|grep single-sign-on-kratos-admin | awk '{print $3'}`
public=`ssh $host -lroot kubectl get service -n oas|grep single-sign-on-kratos-public | awk '{print $3}'`
hydra=`ssh $host -lroot kubectl get service -n oas|grep single-sign-on-hydra-admin | awk '{print $3}'`
psql=`ssh $host -lroot kubectl get service -n oas|grep single-sign-on-postgres|grep -v headless | awk '{print $3}'`
echo "
kratos admin port will be at localhost: 8000
kratos public port will be at localhost: 8080
hydra admin port will be at localhost:4445
psql port will be at localhost:5432
"
echo "Admin port will be at localhost:8000, public port will be at localhost:
8080"
ssh -L 8000:$admin:80 -L 8080:$public:80 root@$host
ssh -L 8000:$admin:80 -L 8080:$public:80 -L 4445:$hydra:4445 -L 5432:$psql:5432 root@$host
......@@ -128,24 +128,12 @@ kratos:
}
},
"name": {
"type": "object",
"properties": {
"first": {
"type": "string",
"title": "First name"
},
"last": {
"type": "string",
"title": "Last name"
}
}
"type": "string",
"title": "Full name",
},
"admin": {
"type": "string"
"type": "boolean"
},
"totp": {
"type": "string"
}
},
"required": ["email"],
"additionalProperties": false
......
import logging
import os
import click
import pprint
# Flask
from flask import abort, Flask, redirect, request, render_template
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from flask.cli import AppGroup
# Hydra admin
import hydra_client
# Kratos ?
#from kratos import User, KratosError
#from forms import LoginForm
import ory_kratos_client
from ory_kratos_client.api import metadata_api
from ory_kratos_client.api import v0alpha2_api as kratos_api
from ory_kratos_client.model.generic_error import GenericError
from ory_kratos_client.model.inline_response200 import InlineResponse200
from ory_kratos_client.model.inline_response2001 import InlineResponse2001
from ory_kratos_client.model.inline_response503 import InlineResponse503
from ory_kratos_client.model.admin_create_identity_body import AdminCreateIdentityBody
from ory_kratos_client.model.admin_create_self_service_recovery_link_body import AdminCreateSelfServiceRecoveryLinkBody
from ory_kratos_client.model.identity_state import IdentityState
#from ory_kratos_client.api.public_api import PublicApi as KratosPublicApi
##from ory_kratos_client.api.admin_api import AdminApi as KratosAdminApi
# Initaliaze the FLASK app
app = Flask(__name__)
# Load config
app.config.from_object(os.environ['APP_SETTINGS'])
# Move to config?
app.logger.setLevel(logging.INFO)
# Create HYDRA & KRATOS API interfaces
HYDRA = hydra_client.HydraAdmin(app.config["HYDRA_ADMIN_URL"])
# Kratos has an admin and public end-point. We create an API for them
# both. The kratos implementation has bugs, which forces us to set
# the discard_unkonwn_keys to True.
tmp = ory_kratos_client.Configuration(host=app.config["KRATOS_ADMIN_URL"],
discard_unknown_keys= True)
KRATOS_ADMIN = kratos_api.V0alpha2Api(ory_kratos_client.ApiClient(tmp))
tmp = ory_kratos_client.Configuration(host=app.config["KRATOS_PUBLIC_URL"],
discard_unknown_keys = True)
KRATOS_PUBLIC = kratos_api.V0alpha2Api(ory_kratos_client.ApiClient(tmp))
#
#ids = KRATOS_ADMIN.admin_list_identities()
#print (ids)
#
# Create DB & migrate interface
db = SQLAlchemy(app)
migrate = Migrate(app, db)
# Import models
from models import User, App, AppRole
##############################################################################
# CLI INTERFACE #
##############################################################################
# Define Flask CLI command groups and commands
user_cli = AppGroup('user')
app_cli = AppGroup('app')
## CLI APP COMMANDS
@app_cli.command('create')
@click.argument('slug')
@click.argument('name')
def create_app(slug, name):
app.logger.info("Creating app definition: {1} ({0})".format(slug, name))
obj = App()
obj.name = name
obj.slug = slug
db.session.add(obj)
db.session.commit()
@app_cli.command('list')
def list_app():
app.logger.info("Listing apps")
apps = App.query.all()
for obj in apps:
print("App name: %s \t Slug: %s" %(obj.name, obj.slug))
@app_cli.command('delete',)
@click.argument('slug')
def delete_app(slug):
app.logger.info("Trying to delete app: {0}".format(slug))
obj = App.query.filter_by(slug=slug).first()
if not obj:
app.logger.info("Not found")
return
db.session.delete(obj)
db.session.commit()
app.logger.info("Success")
return
app.cli.add_command(app_cli)
## CLI USER COMMANDS
@user_cli.command('create')
@click.argument('email')
def create_user(email):
app.logger.info("Creating user with email: ({0})".format(email))
obj = User()
obj.email = email
body = AdminCreateIdentityBody(
schema_id="default",
traits={'email':email},
) # AdminCreateIdentityBody | (optional)
#state=IdentityState("active"),
kratos_obj = KRATOS_ADMIN.admin_create_identity(admin_create_identity_body=body)
obj.kratos_id = kratos_obj.id
db.session.add(obj)
db.session.commit()
@user_cli.command('list')
def list_user():
app.logger.info("Listing users")
users = User.query.all()
for obj in users:
print("Email: %s (admin: %s)" %(obj.email, obj.admin))
@user_cli.command('delete',)
@click.argument('email')
def delete_user(email):
app.logger.info("Trying to delete user: {0}".format(email))
obj = User.query.filter_by(email=email).first()
if not obj:
app.logger.info("Not found")
return
db.session.delete(obj)
# if obj.kratos_id:
#
#
db.session.commit()
app.logger.info("Success")
return
@user_cli.command('setadmin')
@click.argument('email')
def setadmin_user(email):
app.logger.info("Trying to make user into admin: {0}".format(email))
obj = User.query.filter_by(email=email).first()
if not obj:
app.logger.info("Not found")
return
obj.admin = True
db.session.commit()
app.logger.info("Success")
return
@user_cli.command('unsetadmin')
@click.argument('email')
def unsetadmin_user(email):
app.logger.info("Trying to make user into normal user: {0}".format(email))
obj = User.query.filter_by(email=email).first()
if not obj:
app.logger.info("Not found")
return
obj.admin = False
db.session.commit()
app.logger.info("Success")
return
@user_cli.command('recover')
@click.argument('email')
def recover_user(email):
app.logger.info("Trying to sent recover email for user: {0}".format(email))
obj = User.query.filter_by(email=email).first()
if not obj:
app.logger.info("Not found")
return
if not obj.kratos_id:
app.logger.info("User found, but no kratos ID")
return
body = AdminCreateSelfServiceRecoveryLinkBody(
expires_in="1h",
identity_id=obj.kratos_id,
)
# example passing only required values which don't have defaults set
# and optional values
try:
# Create a Recovery Link
api_response = KRATOS_ADMIN.admin_create_self_service_recovery_link(
admin_create_self_service_recovery_link_body=body)
pprint.pprint(api_response)
except ory_kratos_client.ApiException as e:
print("Exception when calling V0alpha2Api->admin_create_self_service_recovery_link: %s\n" % e)
return
app.cli.add_command(user_cli)
##############################################################################
# WEB ROUTES #
##############################################################################
@app.route('/login', methods=['GET', 'POST'])
def login():
"""Provides login form and handles login attempt
Args:
login_form: contains login data submitted by a user (POST)
challenge: id that identifies the request from oauth client. passed by hydra
Returns:
Error page if no challenge id is present
or Login Form if user hasn't authenticated
or redirect to callback url provided by hydra if login was successful
"""
login_form = LoginForm()
redirect_to = None
# TODO: Empty/short passwors is a form Error
challenge = None
# Retrieve the challenge id from the request. Depending on the method it is
# saved in the form (POST) or in a GET variable. If this variable is not set
# we can not continue.
if request.method == 'GET':
challenge = request.args.get("login_challenge")
if request.method == 'POST':
challenge = request.args.post("login_challenge")
if not challenge:
app.logger.error("No challange given. Error in request")
abort(404)
# Now that we have the challenge id, we can request the challenge object from the hydra
# admin API
try:
login_request = HYDRA.login_request(challenge)
except hydra_client.exceptions.NotFound:
app.logger.error("Not Found. Login request not found. challenge={0}".format(challenge))
abort(404)
except hydra_client.exceptions.HTTPError:
app.logger.error("Conflict. Login request has been used already. challenge={0}".format(challenge))
abort(503)
# Skip, if true, let's us know that Hydra has already successfully authenticated
# the user. we don't need to check anything and we can accept the request right away.
if login_request.skip:
app.logger.info("{0} is already logged in. Skip authentication".format(login_request.subject))
return redirect(login_request.accept(login_request.subject))
#skip = request.args.get("skip")
#logout = request.args.get("logout")
#if skip:
# app.logger.info("{0} is already logged in. Skip authentication".format(login_request.subject))
# return redirect(login_request.accept(login_request.subject))
#elif logout:
# login_form.challenge.data = challenge
# HYDRA.invalidate_login_sessions(login_request.subject);
# return redirect(login_request.reject(
# "Login cancelled",
# error_description="Login was cancelled and user session was terminated"))
#else:
# return render_template('skip.html', challenge=challenge, logo=login_request.client.logo_uri, application_name=login_request.client.client_name, username=login_request.subject)
else:
app.logger.info("Not yet logged in. Requesting login credentials")
if "ory_kratos_session" not in request.cookies:
app.logger.info("No kratos cookie, get kratos cookie")
kratos = ory_kratos_client.ApiClient(app.config["KRATOS_PUBLIC_URL"])
login_flow = LoginFlow(app.config["KRATOS_PUBLIC_URL"])
return_to = f"{app.config['PUBLIC_URL']}/login?login_challenge={challenge}"
# We need to decide here whether we want to accept or decline the login request.
# if a login form was submitted, we need to confirm that the userdata, the agent
# send us via POST is valid
if login_form.validate_on_submit():
# Load users
try:
user = User(login_form.username.data)
except KratosError as error:
app.logger.error(
"User does not exists {0}".format(error))
return redirect(login_request.reject(
"Login denied",
error_description="Login request was denied due to an internal server error"))
# Authenticate / TOTP?
if user.authenticate(login_form.password.data):
redirect_to = login_request.accept(
user.username,
remember=login_form.remember.data,
# Remember session for 12h
remember_for=60*60*12)
app.logger.info("{0} logged in successfully".format(user.username))
else:
# Should be internal
redirect_to = login_request.reject(
"Login denied.",
error_description="Invalid username or password")
app.logger.info("{0} failed to login".format(user.username))
return redirect(redirect_to)
# If Skip is not true and the user has not submitted any data via a form, we need
# to display a login form for the user to type in their username and password.
# as a reference we save the challenge id in a hidden field of the form.
else:
login_form.challenge.data = challenge
return render_template('login.html', login_form=login_form, logo=login_request.client.logo_uri, application_name=login_request.client.client_name)
@app.route('/consent', methods=['GET', 'POST'])
def consent():
"""Checks user app permission
Checks user app permission by loading a consent object via the Hydra admin API and
validating that the user triggering the request has sufficient permissions by querying
the GraphQL API. If the user is allowed to use the app the request is accepted and openID
claims are sent to Hydra.
Args:
consent_challenge: Reference to a consent challenge object in form of an alphanumeric
String. Can be used to retrieve the consent challenge object via the Hydra Admin API (GET)
Returns:
Redirect to the url that is provided by the consent challenge object.
"""
challenge = request.args.get("consent_challenge")
if not challenge:
abort(403)
try:
consent_request = HYDRA.consent_request(challenge)
except hydra_client.exceptions.NotFound:
app.logger.error("Not Found. Consent request not found. challenge={0}".format(challenge))
abort(404)
except hydra_client.exceptions.HTTPError:
app.logger.error("Conflict. Consent request has been used already. challenge={0}".format(challenge))
abort(503)
app_name = consent_request.client.client_name
username = consent_request.subject
app.logger.info("Providing consent to %s for %s", (app_name, username))
try:
user = User(username)
except KarosError as error:
app.logger.error(
"Retrieving user object from GraphQL server failed {0}".format(error))
return redirect(consent_request.reject(
"Permission denied",
error_description="Login request was denied due to an internal server error"))
#access_granted = user.has_app_permission(app_name)
if True:
app.logger.info("{0} was granted access to {1}".format(username, app_name))
session = user.get_oauth_session()
return redirect(consent_request.accept(
grant_scope=consent_request.requested_scope,
grant_access_token_audience=consent_request.requested_access_token_audience,
session=session,
))
app.logger.info("{0} was denied access to {1}".format(username, app_name))
return redirect(consent_request.reject(
"Permission denied",
error_description="Login request was denied due to missing application permission"))
if __name__ == '__main__':
app.run()
import os
basedir = os.path.abspath(os.path.dirname(__file__))
class Config(object):
DEBUG = False
TESTING = False
CSRF_ENABLED = True
SECRET_KEY = os.urandom(16)
SQLALCHEMY_DATABASE_URI = os.environ['DATABASE_URL']
SQLALCHEMY_TRACK_MODIFICATIONS = False
HYDRA_ADMIN_URL = os.environ['HYDRA_ADMIN_URL'];
KRATOS_ADMIN_URL = os.environ['KRATOS_ADMIN_URL'];
KRATOS_PUBLIC_URL = os.environ['KRATOS_PUBLIC_URL'];
class ProductionConfig(Config):
DEBUG = False
class StagingConfig(Config):
DEVELOPMENT = True
DEBUG = True
class DevelopmentConfig(Config):
DEVELOPMENT = True
DEBUG = True
class TestingConfig(Config):
TESTING = True
from wtforms import SubmitField, StringField, PasswordField, BooleanField, HiddenField, validators
from flask_wtf import FlaskForm
class LoginForm(FlaskForm):
username = StringField("Username", validators=[validators.input_required()],)
password = PasswordField("Password", validators=[validators.input_required()])
challenge = HiddenField("challenge")
remember = BooleanField("Remember me")
submit = SubmitField("Sign in")
import logging
from flask_login import UserMixin
class User(UserMixin):
def __init__(self, username):
self.logger = logging.getLogger(__name__)
ch = logging.StreamHandler()
self.logger.addHandler(ch)
self.logger.setLevel(logging.DEBUG)
self.id = username
self.username = username
self.logger.info("User is trying to login: " + username)
# TODO Make sure it's a username?
return None
def authenticate(self, password):
self.logger.info("User is providing password: " + password)
# TODO: Accept all password, yeah!
return True
def get_oauth_session(self):
"""Create openID Connect token
Use the userdata stored in the user object to create an OpenID Connect token.
The token will be passed to Hydra, which will store it and serve it to all OpenID Connect
Clients, that successfully query the /userinfo endpoint. Every field in the "id_token"
dictionary can be accessed through standard scopes and claims.
See https://openid.net/specs/openid-connect-core-1_0.html#Claims
Returns:
OpenID Connect token of type dict
"""
return {
"id_token": {
"name": self.username,
"preferred_username": self.username,
"email" : self.username + "@domain.com",
"openappstack_roles": ""}
}
class KratosError(Exception):
"""Raised when requests to the backend server fail
Attributes:
code -- http response code
headers -- http response headers
reason -- reson for the error
"""
def __init__(self, code, headers, reason):
self.code = code
self.headers = headers
self.reason = reason
# raise KratosError(
# 1,
# 2,
# ("Error during retrieval of userdata - " ))
#
# self.active = False
# try:
# self._load_remote_user_info()
# except urllib.error.HTTPError as error:
# raise KratosError(
# error.code,
# error.headers,
# ("Error during retrieval of userdata - " + error.reason))
import os
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from app import app, db
app.config.from_object(os.environ['APP_SETTINGS'])
migrate = Migrate(app, db)
manager = Manager(app)
manager.add_command('db', MigrateCommand)
if __name__ == '__main__':
manager.run()
Single-database configuration for Flask.
# A generic, single database configuration.
[alembic]
# template used to generate migration files
# file_template = %%(rev)s_%%(slug)s
# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false
# Logging configuration
[loggers]
keys = root,sqlalchemy,alembic,flask_migrate
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARN
handlers = console
qualname =
[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[logger_flask_migrate]
level = INFO
handlers =
qualname = flask_migrate
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S
from __future__ import with_statement
import logging
from logging.config import fileConfig
from flask import current_app
from alembic import context
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config
# Interpret the config file for Python logging.
# This line sets up loggers basically.
fileConfig(config.config_file_name)
logger = logging.getLogger('alembic.env')
# add your model's MetaData object here
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
config.set_main_option(
'sqlalchemy.url',
str(current_app.extensions['migrate'].db.get_engine().url).replace(
'%', '%%'))
target_metadata = current_app.extensions['migrate'].db.metadata
# other values from the config, defined by the needs of env.py,
# can be acquired:
# my_important_option = config.get_main_option("my_important_option")
# ... etc.
def run_migrations_offline():
"""Run migrations in 'offline' mode.
This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.
Calls to context.execute() here emit the given string to the
script output.
"""
url = config.get_main_option("sqlalchemy.url")
context.configure(
url=url, target_metadata=target_metadata, literal_binds=True
)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online():
"""Run migrations in 'online' mode.
In this scenario we need to create an Engine
and associate a connection with the context.
"""
# this callback is used to prevent an auto-migration from being generated
# when there are no changes to the schema
# reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html
def process_revision_directives(context, revision, directives):
if getattr(config.cmd_opts, 'autogenerate', False):
script = directives[0]
if script.upgrade_ops.is_empty():
directives[:] = []
logger.info('No changes in schema detected.')
connectable = current_app.extensions['migrate'].db.get_engine()
with connectable.connect() as connection:
context.configure(
connection=connection,
target_metadata=target_metadata,
process_revision_directives=process_revision_directives,
**current_app.extensions['migrate'].configure_args
)
with context.begin_transaction():
context.run_migrations()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}
# revision identifiers, used by Alembic.
revision = ${repr(up_revision)}
down_revision = ${repr(down_revision)}
branch_labels = ${repr(branch_labels)}
depends_on = ${repr(depends_on)}
def upgrade():
${upgrades if upgrades else "pass"}
def downgrade():
${downgrades if downgrades else "pass"}
"""empty message
Revision ID: 207081778bb2
Revises:
Create Date: 2021-11-16 08:18:27.697307
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '207081778bb2'
down_revision = None
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('apps',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('name', sa.String(), nullable=True),
sa.Column('slug', sa.String(), nullable=True),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('slug')
)
op.create_table('users',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('email', sa.String(), nullable=True),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('email')
)
op.create_table('user_app_roles',
sa.Column('user_id', sa.Integer(), nullable=False),
sa.Column('app_id', sa.Integer(), nullable=False),
sa.Column('role', sa.String(), nullable=True),
sa.ForeignKeyConstraint(['app_id'], ['apps.id'], ),
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
sa.PrimaryKeyConstraint('user_id', 'app_id')
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('user_app_roles')
op.drop_table('users')
op.drop_table('apps')
# ### end Alembic commands ###
"""empty message
Revision ID: 9817fa267190
Revises: 207081778bb2
Create Date: 2021-11-16 08:41:26.106815
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '9817fa267190'
down_revision = '207081778bb2'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('users', sa.Column('admin', sa.Boolean(), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('users', 'admin')
# ### end Alembic commands ###
"""empty message
Revision ID: ca13699053b3
Revises: 9817fa267190
Create Date: 2021-11-16 10:34:23.890576
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'ca13699053b3'
down_revision = '9817fa267190'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('users', sa.Column('kratos_id', sa.String(), nullable=True))
op.create_unique_constraint(None, 'users', ['kratos_id'])
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint(None, 'users', type_='unique')
op.drop_column('users', 'kratos_id')
# ### end Alembic commands ###
from app import db
from sqlalchemy.dialects.postgresql import JSON
from sqlalchemy.orm import relationship
from sqlalchemy import Column, Integer, String, ForeignKey, Boolean
class User(db.Model):
__tablename__ = 'users'
id = db.Column(Integer, primary_key=True)
email = db.Column(String, unique=True)
kratos_id = db.Column(String, unique=True)
admin = db.Column(Boolean, default=False)
app_roles = relationship('AppRole', back_populates="user")
def __repr__(self):
return '<id {}>'.format(self.id)
class App(db.Model):
__tablename__ = 'apps'
id = db.Column(Integer, primary_key=True)
name = db.Column(String())
slug = db.Column(db.String(), unique=True)
def __repr__(self):
return '<id {}>'.format(self.id)
class AppRole(db.Model):
__tablename__ = 'user_app_roles'
user_id = db.Column(Integer, ForeignKey('users.id'), primary_key=True)
user = relationship("User", back_populates="app_roles")
app_id = db.Column(Integer, ForeignKey('apps.id'),
primary_key=True)
app = relationship("App")
role = db.Column(String)
Flask
flask-wtf
flask-login
hydra-client
Flask-SQLAlchemy
Flask-Migrate
Flask-Script
psycopg2
ory-kratos-client
#!/bin/sh
. ./source_env
flask run
\ No newline at end of file
#!/bin/sh
export FLASK_RUN_HOST=0.0.0.0
export FLASK_RUN_PORT=5000
export HYDRA_ADMIN_URL=http://localhost:4445
export KRATOS_PUBLIC_URL=http://localhost:8080
export KRATOS_ADMIN_URL=http://localhost:8000
export DATABASE_URL="postgresql://stackspin:stackspin@localhost/stackspin"
export APP_SETTINGS="config.DevelopmentConfig"
<!doctype html>
<title>OAS authentication service</title>
<div style='margin: 0 auto ; width: 350px; padding:20px; border-style:solid; border-color:#6c757d; border-width: 1px; background-color: #f8f9fa; font-family: "Segoe UI", Roboto; font-family: "Helvetica Neue", Arial; font-family: "Noto Sans", sans-serif;'>
{% if logo %}
<div style="position:relative; width: 350px; height:100px">
<img style="overflow: auto; top: 0; left: 0; bottom: 0; right: 0; position: absolute; margin: auto;max-width: 300px; max-height: 100px" src="{{logo}}" alt="Logo of application"></img>
</div>
{% endif %}
<h1>Log in to {{ application_name }}</h1>
<form method="POST" action="/login">
{{ login_form.csrf_token }}
{{ login_form.challenge }}
{{ login_form.username(placeholder="Username", autofocus=true) }}<br>
<div style="margin-top:5px">{{ login_form.password(placeholder="Password") }}</div> <br>
{{ login_form.remember }}{{ login_form.remember.label }} <br>
<div style="margin-top:5px">{{ login_form.submit }}</div>
</form>
</div>
<!doctype html>
<title>OAS authentication service</title>
<div style='margin: 0 auto ; width: 350px; padding:20px; border-style:solid; border-color:#6c757d; border-width: 1px; background-color: #f8f9fa; font-family: "Segoe UI", Roboto; font-family: "Helvetica Neue", Arial; font-family: "Noto Sans", sans-serif;'>
{% if logo %}
<div style="position:relative; width: 350px; height:100px">
<img style="overflow: auto; top: 0; left: 0; bottom: 0; right: 0; position: absolute; margin: auto;max-width: 300px; max-height: 100px" src="{{logo}}" alt="Logo of application"></img>
</div>
{% endif %}
<h1>Log in to {{ application_name }}</h1>
<div style="width: 100%; margin-bottom: 5px; overflow: auto">
<div style="width:60%; float:left"><button id="continue" onclick="window.location.href = '/login?login_challenge={{ challenge }}&skip=true';">Continue with {{ username }}</button></div>
<div style="width:40%; float:left;"><button id="logout" onclick="window.location.href = '/login?login_challenge={{ challenge }}&logout=true';">Logout</button></div>
</div>
</div>
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment