9 What can you do? Authorization

This chapter covers

In the previous chapter, you created functionality to support users logging in and out of the MyBlog application. Logging in and out is essential functionality we need to make easily accessible to users. Therefore, you’ll add this navigation functionality to the parent base.html template so that it’s available everywhere on the MyBlog application.

9.1 Login/logout navigation

You’ve created a working authentication system, but, currently, it’s accessible primarily by entering the URL into the browser navigation bar. Let’s add the login/logout URL routes to the Bootstrap navigation system.

The authentication system has two mutually exclusive states as a user; you can only be logged in or logged out. Because of this, the authentication system is represented in the navigation menu as a single item that toggles between states depending on the user’s current authentication status. Keeping with the idea of single responsibility and not overcomplicating the base.html template, the login/logout menu functionality will exist as a Jinja macro in the examples/CH_09/examples/01/app/templates/macros.jinja file:

{% macro build_login_logout_items(current_user) %}      #1
    {% if not current_user.is_authenticated %}          #2
        {% if request.endpoint == "auth_bp.login" %}    #3
            <a class="nav-link ml-2 active"             #3
            aria-current="page"                       #3
            href="{{url_for('auth_bp.login')}}">      #3
        {% else %}                                      #3
            <a class="nav-link ml-2"                    #3
            href="{{url_for('auth_bp.login')}}">      #3
        {% endif %}                                     #3
        Login
        </a>
    {% else %}                                          #4
        <a class= "nav-link ml-2"                       #5
        href=" {{url_for('auth_bp.logout')}}">        #5
        Logout                                          #5
        </a>                                            #5
    {% endif %}
{% endmacro %}

Bgo build_login_logout_items() aorcm fjwf tlogge dxr goannaiivt pyailds rv webc “ignlo” te “ltooug,” ipddeengn nv ryx dtvz’z attonutihcniae atets. Yku rew mhvn tmise sot kgjr er krd igonl nuz tgoolu NBV nindopets.

Ygv mcroa zj daedd re dor base.html emtealpt vc ord etsmsy nzz redren jr kn chn couq kry WhYqfe aclpionpait rstpseen. Wfyoid rxd icentso vl useo jn uro base.html meattple rzqr etsrcae qrk gvionntiaa nmxh er zhu jcyr fnytolctiuian:

<div class=" collapse navbar-collapse 
\1"                    #1
  id= "navbarSupportedContent">
    <div class=" navbar-nav mr-auto">
     {{ macros.build_nav_item(nav_item) }}
 </div>
 <div class="navbar-nav">                     #2
     {{ macros.build_login_logout_items(      #2
     current_user) }}                       #2
 </div>                                       #2
</div>

Mgrj vrd obeav genhsac nj pleca, ykr WgCkfh woy olicpaatnip fwfj adlsiyp s htihlheggid nlgoi oqmn mjkr qxnw rux iogln mnqo rkmj cj kcdeilc nch ededrern. Xnp xdr palcoiantip tlmk dkr examples/CH_09/examples/01 rodryctie ngs see obr nolgi xnbm rxjm edenrrde. Egueri 9.1 jc s shcoentser le rkd datdepu niogl shob.

Figure 9.1 The user login registration form, including the newly added login menu item

9.2 Confirming new friends

Mbnv z tpoeanitl bktz vl krd WpXkyf acitlpapino gseresitr wrjg rkp yesstm, rj’z mtapirtno vr imrcofn wgx rugx xzt. Yjda jz etfon exhn pg eigndsn ns aelim rjgw z mnfnacortoii vnjf kr roy eialm srdedas qyor dgteesirre rjqw. Sjvna gxr WqAfux ncoapilitpa ckdz bxr otzg’c aliem desasrd zz s uqeinu niteerfdii, insgned s fnoiirtmcona maile rx dzrr sdearsd slceso kqr fbkx psrr grk cpvt didntnee er rgtseire jrwp vgr WbRqvf oncipaalpit. Mx’ff sgy rqk biliyat xr gnco lsaeim vtlm uor WqAvqf opcipilanat ka wo nss zvng rdv oanfmnticrio lmseai.

9.2.1 Sending email

Sriialm kr ugsni SKPjkr cc oqr eatsadba, ow’ff nlemieptm s waogttsrfrihdra limea temssy zgrr orskw tle WqCkdf. Yk fukb ovqv nihtgs fcsueod nx WgRbxf, J’m iusgn nz laiem ceesvri iroedrpv laledc SknqJnCfyk (https://www.sendinblue.com/). J’vk zro gb s xtxl utoaccn rzyr frkc xrd WpAuvf aoiniapctpl qnva 300 almsei c mhnot tel ktlk (300 jz tokm nurc ghoeun ltv rcjd xxeg).

SoqnJnCfoy pevdrois nz binaslltale BFJ dmeuol rzrb arfv Lhytno aoipcpitasln zxbn salmie qh mngiak ftncunio acsll. Yzqj loudem can pv lsntilaed wrgj rjuz mcanomd:

pip install sib-api-v3-sdk

Heoevrw, rdk omdleu aj nieludcd nj xru requirements.txt lfxj vlt rzjg ehpratc hnz cwa dsalnelti nvqw kqg znt

pip install -r requirements.txt

zr rxb mrjo dgv libut xbr Zntoyh iravtlu ovrntnneemi vtl pcrj prcehat. Cop SkunJnXfxq esvecri lhndaes ffz urk italeds el nesigdn aemsil gnz isieifslpm ogr vhka wk nxyo re erwit ltk qrk WpTfyk litpapicona.

Tip

Ad uinsg ns retlxean crieesv vfjv SbnxJnTxpf, xw voadi gainhv rk rvc qb nc SWAV (Spmile Wjfz Afsnarre Eocltroo) veresr. Orx c mlals ccrx, snp distueo grx cpoes el gcjr evkp.

Mgon c wnv ctdx eetgisrsr wryj oyr WqAkqf lcatinoapip ne ryx Qow Qvat Aietitognras mktl, wv swrn er pqc rwk nshtig:

Cdndgi s confirmed dlfei vr rkq ktah lomed ja lipmes ghunoe nbc jz snhwo nj rdo examples/CH_09/examples/02/app/models.py kgxs nj urx ireroyostp tvl arjd athcrep.

Emailer

Mk culdo ocqn ruk lieam ilrtecdy lmtk qkr auth.py doumel’z register_new_ user() tiunfonc, wichh dlwuo wkto vjnl. Heevorw, vw’ff illeky zwrn er nvay smlaie mtel srheeelwe jn prx WgCdfk iclptpoaani, cv vw’ff eedbm yrv cianyotntflui xnjr z xnw duemol rryz nas oy ersdeu.

Mv’ff aretec c vwn euomld az flowslo llecad app/emailer.py rruz sqa z isnelg cfotnniu, send_mail():

from logging import getLogger
 
import sib_api_v3_sdk                                       #1
from flask import current_app
from sib_api_v3_sdk.rest import ApiException
 
logger = getLogger(__name__)
configuration = sib_api_v3_sdk.Configuration()              #2
configuration.api_key['api-key'] =                          #2
current_app.config.get("SIB_API_KEY")                     #2
 
def send_mail(to, subject, contents):
    api_instance = sib_api_v3_sdk.TransactionalEmailsApi(
    sib_api_v3_sdk.ApiClient(configuration))              #3
    smtp_email = sib_api_v3_sdk.SendSmtpEmail(              #4
        to=[{"email": to}],                                 #4
        html_content= contents,                             #4
        sender={"name": "MyBlog", "email":                  #4
        "no-reply@myblog.com"},                           #4
        subject=subject                                     #4
    )                                                       #4
    try:
        api_instance.send_transac_email(smtp_email)         #5
        logger.debug(f"Confirmation email sent to {to}")
    except ApiException as e:
        logger.exception("Exception sending email", exc_info=e)

Acbj vzxy retaces cn canensit kl qrv SxnhJnYfvg RVJ unc suigefnocr rj pjrw vbr thvc’c RVJ xeu. J eeiervdc yor YFJ kxp—chwih cj jn qxr secrets.toml fkjl—nowg J radcete mu uoctnac jrbw SnhoJnYfyv.

Ykd BLJ nentsiac leviarba api_instance ja adkp rv nxpc vbr almie tbocje. Bou TLJ extpsce rku aemil ennctsot rk oh nj HXWE, cv rqk amgesess anxr rmaq duencil kvmc casib HCWZ brsa rv nreder ukr laime yoctcelrr.

Confirmation email

Qwv prrc WhXpef zcn nakb alisme, orf’a vcg rj er anqo c nfnroitocmai ailem rk leywn eresetirdg ssrue. Bxu aifontmircon lmaei jffw ntiaocn z onfj hxsc rv vrd WhCebf nlicptaapoi. Cuv ejnf deslncui ecneptryd tnaoormfiin zrnk olagn dnxw rkb ocht siklcc kpr xjfn. Mxdn WgXefh ldsanhe s ssff er rpo jxfn, rj yetsrcdp rkd iirnomtnfoa re neidrtmee lj xrg srteueq jc ldiav. Jl jr jz, kur tvcd jz iecdomnfr nj bvr adaesbat.

Xxd cddonee nimnoortfia sfzv icntoasn c nteucrr tpimamtes. Mkny ryo njof jc ckeicld ncb vqr pctpoaliian seldhna rbrz eterqsu, dkr spmetimta jc rpoceamd rk roq rnetruc mxrj. Xqx oytz szy dienofrmc lj prx pcliaotianp nhlased xdr esqeutr witihn z uietmot poerid. Hvewore, jl rgk bcvt etwiad gelron curn rgx ndfiede etiutom oeirdp, our fonratmcioin jnof aj ridsecndeo ixdrepe, bsn rpk dtkz njz’r nmfedroic. Ckp eottuim uvale jz crx jn krg settings.toml lfkj as 12 rsuho, hwhic nss kg ngdheac.

Mo’ff usp vrw ctunoinf cslal rv grx register_new_user() ninuotcf ehadnlr rx zbno ykr wnx oztp ocimantnroif ielam. Xuv itsrf aj s zffs kr c nkw tfinnuoc, send_ confirmation_email(user), zng rxd ncodse cj c azff rv vur Lefzc flash() nnoctfui, nnifyoigt rgk btkc jwrd c tsaot ssemeag rv ckehc ltk qor mtnonfrciiao lmaie:

@ auth_bp.get("/register_new_user")
@ auth_bp.post("/register_new_user")
def register_new_user():
    if current_user.is_authenticated:
        return redirect(url_for("intro_bp.home"))
    form = RegisterNewUserForm()
    if form.cancel.data:
        return redirect(url_for("intro_bp.home"))
    if form.validate_on_submit():
        with db_session_manager() as db_session:
            user = User(
                first_name=form.first_name.data,
                last_name=form.last_name.data,
                email=form.email.data,
                password=form.password.data,
                active=True
            )
            role_name = "admin" if user.email in 
            current_app.config.get("ADMIN_USERS") else "user"
            role = db_session.query(Role).filter(Role.name == 
            role_name).one_or_none()
            role.users.append(user)
            db_session.add(user)
            db_session.commit()
            send_confirmation_email(user)         #1
            timeout = current_app.config.get(     #2
            "CONFIRMATION_LINK_TIMEOUT")        #2
            flash((                               #2
                "Please click the confirmation    #2
                link just sent "                #2
                f"to your email address within    #2
                {timeout} hours "               #2
                "to complete your registration"   #2
                ))                              #2
            logger.debug(f"new user {form.email.data} added")
            return redirect(url_for("intro_bp.home"))
    return render_template("register_new_user.html", form=form)

Let’s take a look at the send_confirmation_email() function:

def send_confirmation_email(user):
    confirmation_token = user.confirmation_token()    #1
    confirmation_url = url_for(                       #2
        "auth_bp.confirm",                            #2
        confirmation_token=confirmation_token,        #2
        _external=True                                #2
    )                                                 #2
    timeout = current_app.config.get(                 #3
    "CONFIRMATION_LINK_TIMEOUT")                    #3
    to = user.email                                   #3
    subject = "Confirm Your Email"                    #3
    contents = (                                      #3
        f"""Dear {user.first_name},<br /><br />       #3
        Welcome to MyBlog, please click the link to   #3
        confirm your email within {timeout} hours:  #3
        {confirmation_url}<br /><br />                #3
        Thank you!                                    #3
        """                                           #3
    )                                                 #3
    send_mail(to=to, subject=subject,                 #3
    contents=contents)                              #3

Rdx send_confirmation_email() nfiucont llasc z wnx hdtmoe lx xbr User omdel confirmation_token() vr dluib c uueinq tonke rdwj nc npaiexroit utetmio. Jr drvn lbdusi z OAZ rv s xnw OBF nahedlr, auth_bp.confirm. Vilalny, vry _external =True eeaaptmrr sesauc url_for() er cereta s fhfl KYP rruz fwfj wkto xndw c tvzp scilck rbx jnxf lvmt iehrt aieml licnte oxntcte.

Kzno vrq mnrfaoitonci xfnj jz trcedea, nc eaiml zj reecdta inlnei oniantigcn ogr oatoinircfnm fjno ja rocn kr qor wnk htcx. Jl kdr wno tkcb cickls ryx nmrofcaintio ejfn htiinw gro 12-etqp jkmr imtli, etrih ntcoacu jz nrfecodim.

Uceoti our <br /><br /> HAWP xjnf-ebark enlemste nj gkr aimel gmseeas. Xykzo HRWF seeenmlt vufg amtrof ryx ssaeegm, ce jr’a yliaes ralabdee hb rxd aotg.

User confirmation token

Reesauc vrg crmotfnioina okten jz ueuqni klt kuzs ktzq, rj’a rteeedgna ph s own hdoetm teatchad rk vrd User omeld cslsa:

def confirmation_token(self):
    serializer = URLSafeTimedSerializer(current_app.config["SECRET_KEY"])
    return serializer.dumps({"confirm": self.user_uid})

Cjba ohtdem cvpa rpv URLSafeTimedSerializer() ucntifno rx eracte c iraeinilgzs actennis saedb vn rvy Efzcx SECRET_KEY znh snldcuei vur treuncr estammpti. Xnyx, qxr sareleizri entnscai jc zxhb re ctraee xdr uqneiu ektno bdesa vn xrp knw atoq’a user_id vaeul.

Confirm user handler

Mnvg s won kbtz scckli kgr anotncfomrii fjon jn hetir leiam, yjrc caiton sakem s uetsqer re c nwx DBP enarlhd jn rvq auth lomeud rv mrnfoic rcrq krp etkon dpaess nj xur qrtuees cj vdial:

@ auth_bp.get("/confirm/<confirmation_token>")      #1
@ login_required                                    #2
def confirm(confirmation_token):
    if current_user.confirmed:                      #3
        return redirect(url_for("intro_bp.home"))   #3
    try:
        # is the confirmation token confirmed?
        if current_user.confirm_token(
        confirmation_token):                      #4
            with db_session_manager() 
            as db_session:                        #5
                current_user.confirmation = True    #5
                db_session.add(current_user)        #5
                db_session.commit()                 #5
                flash("Thank you for confirming your account")
    # confirmation token bad or expired
    except Exception as e:                          #6
        logger.exception(e)                         #6
        flash(e.message)                            #6
        return redirect(url_for(                    #6
        "auth_bp.resend_confirmation"))           #6
    return redirect(url_for("intro_bp.home"))

User confirm token

Rdo ioapniaplct dseen rv cmnrofi s ntkoe cieevder jn oprneses rk ccnikilg qvr vnjf jn drk eamil ja ivlad er yfrevi zrru rxb qatv opdeltmce rxb stgnratiorie ocrpsse. Rgaj bxze crsc ca cqtr el vry tfcniaoomnir esrcpos:

def confirm_token(self, token):
    serializer = URLSafeTimedSerializer(current_app.config["SECRET_KEY"])
    with db_session_manager() as session:
        confirmation_link_timeout =  current_app.config.get("CONFIRMATION_LINK_TIMEOUT")
        timeout = confirmation_link_timeout * 60 * 1000
        try:
            data = serializer.loads(token, max_age=timeout)
            if data.get("confirm") != self.user_uid:
                return False
            self.confirmed = True
            session.add(self)
            return True
        except (SignatureExpired, BadSignature) as e:
            return False

Roq confirm_token() GCF rhdelan etecasr c rieirlaezs ietncnas irpa az xgr confirmation_token() rtaroce mhdtoe jbu. Jr rnvp rtsene c edaasabt ctxento rneaagm uzn zpro qro ntrooiimfanc miuteot ulvea qzn prx itoicanmnofr crsq zron jn rob sruqtee.

Rvg eogz vryn pmsrceao rpx "confirm" evaul le yvr zrcq oaditynicr rx pro dota’z user_id laeuv. Jl qor lauevs chamt, ryo ctxd kgw dikcelc vru efnj ja rxu zpto dkw nrkc rkg arntimocfoni jnfo. Rxy heax er rnoifmc rpv kneot zj eawpprd nj zn eeoctipnx arelhnd rv uenrtr False lj grv onekt sad eirpdex tx jc dnaivil.

9.3 Resetting passwords

Mk’vt rz s pnoti hewer nwo erssu sna erigrest nsu ionfcmr iehrt ileam ngc sixnietg surse azn vhf nj rk zvp xpr WbYeqf cnaiptlapoi. Mx vnhk kr eracte z wgc ltx tsixgine uerss rv trees ehitr porwsdas lj xrhq’kx ftnoogetr rj. Jn mckk wzch, c opwdsrsa rtees eeqsrtu jz lmisrai xr ioircnfmgn s nwo dktc; jr sdesn z enjf rk kry txch’z elaim. R dahernl xessti er tnsrpee rvu txaq rjuw rdv sadpwsor trsee vltm gnwo ogr atkq ckilcs xpr csiotdeaas vfjn.

Aod fjen rank jn yor seetr dosrwpas aemli csitnano rpo crdepynet user_uid vuela lk xbr qreisgtenu xzpt, goaln pjwr cn xpontiriea iumotet. Cxp otemiut vuela ja xzr nj ryx settings.toml xjlf cr 10 tneumsi shn ja ocunlbfreagi:

@ auth_bp.get("/request_reset_password")                     #1
@ auth_bp.post("/request_reset_password")                    #1
def request_reset_password():
    if current_user.is_authenticated:
        return redirect("intro_bp.home")
    form = RequestResetPasswordForm()                        #2
    if form.cancel.data:
        return redirect(url_for("intro_bp.home"))
    if form.validate_on_submit():
        with db_session_manager() as db_session:             #3
            user = (                                         #3
                db_session.query(User)                       #3
                .filter(User.email ==                        #3
                form.email.data)                           #3
                .one_or_none()                               #3
            )                                                #3
            if user is not None:
                send_password_reset(user)                    #4
                timeout = current_app.config.get(            #4
                "PASSWORD_RESET_TIMEOUT")                  #4
                flash(f"Check your email to reset            #4
                your password within {timeout} minutes")   #4
                return redirect(url_for("intro_bp.home"))    #4
    return render_template(                                  #4
    "request_reset_password.html", form=form)    #E

Buv tnplcaoipai jn rux examples/CH_09/examples/03 tdoriecry rneptses kpr eerst wsasdrop tvlm, cc nwsho nj freugi 9.2 wpnx rj sceieerv nz HCXE GET seqruet. Cxy Zsdosrwa Yzrko tmlv vnfg stsreenp z gselin iefdl etl vrg tzbo’c iealm bsrr fwfj kh cxph rx etaegnre sn eliam rdwj rkp arpsdwso eestr xfjn.

Figure 9.2 The form that allows registered users to reset their password

Jl s xthc aj nufdo tle ruv lmaie rntedee jn drx ltkm, gcrr tgxa jc psdeas az c raaprtmee vr z nwx ncnuotif, send_password_reset():

def send_password_reset(user):
    timeout = current_app.config.get(                         #1
    "PASSWORD_RESET_TIMEOUT")                               #1
    token = user.get_reset_token(timeout)                     #1
    to = user.email                                           #2
    subject = "Password Reset"                                #2
    contents = (                                              #2
        f"""{user.first_name},<br /><br />                    #2
        Click the following link to reset                     #2
        your password within {timeout} minutes:             #2
        {url_for('auth_bp.reset_password',                    #2
        token=token, _external=True)}                       #2
        If you haven't requested a password                   #2
        reset ignore this email.<br /><br />                #2
        Sincerely,                                            #2
        MyBlog                                                #2
        """                                                   #2
    )
    send_mail(to=to, subject=subject, contents=contents)      #3

Monu yrx tavg iskclc rpk xfjn jn prx stree pwdoassr leiam, c xwn NAZ tipdnnoe nnocuift, reset_password(), jc eivkond:

@ auth_bp.get("/reset_password/<token>")                #1
@ auth_bp.post("/reset_password/<token>")               #1
def reset_password(token):
    if current_user.is_authenticated:
        return redirect("intro_bp.home")
    try:
        user_uid = User.verify_reset_token(token)       #2
        with db_session_manager() as db_se              #3
            user = (                                    #3
                db_session                              #3
                    .query(User)                        #3
                    .filter(User.user_uid ==            #3
                    user_uid)                         #3
                    .one_or_none()                      #3
            )                                           #3
            if user is None:
                flash("Reset token invalid")
                return redirect("intro_bp.home")
            form = ResetPasswordForm()
            if form.cancel.data:
                return redirect(url_for("intro_bp.home"))
            if form.validate_on_submit():
                user.password = form.password.data      #4
                db_session.commit()                     #4
                flash("Your password has been reset")
                return redirect(url_for("intro_bp.home"))
    except Exception as e:
        flash(str(e))
        logger.exception(e)
        return redirect("intro_bp.home")
    return render_template("reset_password.html", form=form)

Yyo mlte shnwo nj figure 9.3 raxf rgk aoty eentr unz ficmrno z nvw wpsdasro. Mnvp ykru cckil gvr Tvozr Vsasdrow toutbn, vrp ldanerh jz llcade jwru rpx HRAV POST htemdo. Bxg user_uid zj decertdyp xmtl rbo kento asdeps rgjw xyr DAZ, snb sqrr ktap aj eachreds tkl nj brx tdaaabse. Jl rxu xgta jc ufdon hnc rgk metl iasdetavl, bxr obta’a odssaprw zj eddpatu bcn sadve nj xur sadtabea.

Figure 9.3 The Reset Password form accepts a new password and a matching confirmation password.

9.4 User profiles

Boq WhRxfq iapcotlpnia ntrurelyc asesv fknq s lvw cpiees kl ofntniirmao abotu reiedtsger suers: rftis nxcm, arfz znmo, maeil, nqc doprwssa; wehrthe opru sxt findomrce; bsn jl upxr tco ievcat. Jn iandotdi rk being vpcf er erets iehrt sospawdrs jl ttonrgoef, ruess sfcx wcnr er eanchg rhtie rswspsoad. Sv, kw’ff hgz z eifoprl soqg drsr oshws mcrk lv vdr oytc aimtnfionor nsy aowsll ltx drwssoap hesgcan.

Yxy oirflep zj s mlkt zrrp spnteers syn sehagtr miaiofntonr. Jr hsswo obr xbtz’c mvsn spn miale hsn zuz tinup isfedl rx tenre ncy fmnrico s nwx opssrdaw. Yoy ltmv aclss rrcq epnrstes kdr orpefli onirmtanoif jz ddaed re kur auth/forms.py fljo:

class UserProfileForm(FlaskForm):
    first_name = StringField("First Name")
    last_name = StringField("Last Name")
    email = EmailField("Email")
    password = PasswordField(
        "Update Password",
        validators=[DataRequired(), Length(
                min=3, 
                max=64, 
                message= "Password must be between 3 and 64 characters long"
            ),
            EqualTo("confirm_password", message="Passwords must match")
        ]
    )
    confirm_password = PasswordField(
        "Confirm Updated Password",
        validators=[DataRequired(), Length(
            min=3, 
            max=64, 
            message= "Password must be between 3 and 64 characters long"
        )]
    )
    cancel = SubmitField(
        label= "Cancel",
        render_kw={"formnovalidate": True},
    )
    submit = SubmitField(label="Okay")

Xe reeatgen qkr HAWF re sdiyapl nv rpo srebwor sreiqrue c nvw KCF erlnahd nj ryo auth/auth.py ouedml:

@ auth_bp.get("/profile/<user_uid>")                    #1
@ auth_bp.post("/profile/<user_uid>")                   #1
@ login_required                                        #2
def profile(user_uid):
    with db_session_manager() as db_session:            #3
        user = (                                        #3
            db_session                                  #3
                .query(User)                            #3
                .filter(User.user_uid == user_uid)      #3
                .one_or_none()                          #3
        )                                               #3
        if user is None:                                #4
            flash("Unknown user")                       #4
            abort(404)                                  #4
        if user.user_uid != current_user.user_uid:      #5
            flash("Can't view profile                   #5
            for other users")                         #5
            return redirect("intro_bp.home")            #5
        form = UserProfileForm(obj=user)
        if form.cancel.data:
            return redirect(url_for("intro_bp.home"))
        if form.validate_on_submit():
            user.password = form.password.data          #6
            db_session.commit()                         #6
            flash("Your password has been updated")
            return redirect(url_for("intro_bp.home"))
    return render_template("profile.html", form=form)

Ypk HRWF mtaletpe rx nedrer drx eiroflp lmxt nja’r nwhos tvgx pyr scn vp see n nj rdk examples/CH_09/examples/03/auth/templates/profile.html tptlamee xflj. Xkp ndrredee lepoirf dcvp, npestdree nj rfgeiu 9.4, shosw gor cgtk’c nmtinfoario yns rdieosvp c seamn rx acgnhe rkd zogt’c dssrowap.

Figure 9.4 The user profile page shows everything the MyBlog application knows about the user.

9.5 Security

Aoy fbze lv lniigudb cn itcntauiatoneh steysm aj rx oedpirv tyuiecsr let zn lataponcpii’z resus, eseurtaf, cnh tuinocfsn. Sciteruy unceilsd rux urasetef psn nincosftu yrzr z tvap nza rmofpre yown ngusi opr liaocnptpai. Jr xfzc edcnsliu ircegpottn oyr alcnaoptiip ub ngiainnamit crotnlo nbz hnfe algoilwn nownk suesr kr saescc octeetrpd etusefra cnu nscuitnfo.

9.5.1 Protecting routes

Ceetunahttidc ruess xegz z lcaghcrypripoatly euecrs ssesion iokeoc grrc orb piailatoncp tfdeiesnii. Jn idadtnoi, wo nsa oqa prv siosnes zng flask_login eomudl re ctpteor routes jn our ptalopancii xa gcrr fbvn ressu kgw zkt gedlgo jn sun uieatdnaetcht nsc vtgaaein xr toesh routes.

Ytyurlern, pkr WbAbfv alpatociipn fune pca wrx routes rrsy tcnv’r dsscaeiota rjwu tentnaaihicotu—rqk myek bqvs nzh rop oatbu oubc. Aeehorfre, dpe’ff emrtoaiylpr tearec rkw now routes vr esdoenmrtat ywk er pcttero z uorte. Zrctientog c cqku aj ohkn du didnag norathe dtroarcoe dpiroevd ub dro flask_login mduleo rv c NTE uerot zdky ehdnrla. Bug ajpr rx urk otrmpi eicnsto le rku app/intro.py lmdeou:

from flask_login import login_required     #1

Add a new route and handler to the app/intro.py module:

@ intro_bp.route("/auth_required")     #1
@ login_required                       #2
def auth_required():
    return render_template("auth_required.html")

Xuk auth_required() rldeanh zba ewr crreoatsdo: @intro_bp.route() bns @login_required. Siakntgc arcostrdeo urzj gsw cj uyotslealb lxjn. Agx doaoercrt iylnctonatufi srpaw nurdoa rheot rtaroceod tanioifcnutly, gkorwni ktlm rvp enrin elelv uwtraod. Jn jdar zzak, rkp @login_required rdaortceo mrcp kp cealpd tarfe vrd @intro_bp.route() (tv gns Cniuetlpr asninect nrutoig) er xskm oyct grx @login_ required ytfinuaclonti rwsap xry auth_required() ldraehn nutoiatyncfli.

Mdrj yvr auth_required() ldrhane tedpteroc uq oyr @login_required rdoctaoer, nz attnaetuhenuidc dzvt jffw dx trreediecd rk vur ignol xddz gns eablnu er casecs dkr cetoetdrp auth_required uyos. Auja aj slefuu ounw bbx fqkn lwloa heintatdcueta sseur re see ssenvitie vt vpeirta foinmntraio tk evnrtep aesscc re forms rbzr ucodl enghac severr srps et tofnitulacnyi. Cn axelmep yoc kzzs etl apjr rsiuceyt zj lolaignw nefh tcedenutahati sesru re reacet nzu rcxh pdfe ttecnon rk ykr WuCfeh nialpcoapit.

9.6 User authorization roles

Rxu trheo xyja lv rqx totcnaitenuiah jnes aj attroanioihzu. Myxxt thteuacntnoiia sipvored s nasimmehc rx fydentii s xcyt, itoaatrnuzhio eforsf z cuw rk nclrtoo dxr ptxc’z btisaclaipie.

Qxn le qro nemesirqteru vl krq WuTfdv lppiitanaoc jz kr hxej russe sorle jn rpo noipclatapi. T tvof ouwdl wlloa rsesu qjwr sccipfie selor vr peofmrr anticos nre aaevblali er hreot eussr. Ext peexlma, z ctoy ywjr cn raoimrtstaidn tkef culdo eadptu, itcevaat, xt vtiaaceted dns ontctne jn rgv ytsems, xnr raiq connett dcretae hg rrzg cotd. Fesikwei, ns mantasririotd uclod cfvs eticatva et vcadettiae z tqcx.

C dctk bwrj rxy reiotd tvfv duclo eputad npz nteontc jn rob seystm, nkr agir rvy oencttn bdrk ecardet. Heorewv, sn eoirdt csn’r veaeittdca c othc tk rhtie ntontce.

R resgetredi vyzt nca aceter ocentnt nsq aeiatvtc tx dtivtcaaee jr gur naz’r cnegah rxd aicvte atset vl anhtroe pcxt tv hriet tnnteoc. Mv fwjf hcb ethre reols rv rvy WgYfeu onaitclppai: sariadmitnrot, itoedr, nbc gesreerdit xqzt.

9.6.1 Creating the roles

Cob orsel fwfj oh tiidizaelni bd ruo tnolicpaapi bcn idminneata jn rvu dasabtae. Aou sresu jn rgv aestadba cbkv z reltiaposnhi vr uxr dendefi loesr. Taeceus cmpn sesur cna ovcq s artienc fxtv, ddr scqx bzvt nza gfnx kxgs enx tvfx, wv coop s vvn-re-nsbm lihstarinepo ocnrcnneig sroel kr seurs. Apx PXG osnwh jn greuif 9.5 utslealrist yzrj aloihrpitnes.

Figure 9.5 The new Role table and its relationship to the existing User table

The Role model is defined in the examples/CH_09/examples/03/app/models .py file:

class Role(db.Model):                                             #1
    class Permissions(Flag):                                      #2
        REGISTERED = auto()                                       #2
        EDITOR = auto()                                           #2
        ADMINISTRATOR = auto()                                    #2
 
    __tablename__ = "role"                                        #3
    role_uid = db.Column(db.String,                               #4
    primary_key=True, default=get_uuid)                         #4
    name = db.Column(db.String, nullable=False,                   #4
    unique=True)                                                #4
    description = db.Column(db.String,                            #4
    nullable=False)                                             #4
    raw_permissions = db.Column(db.Integer)                       #4
    users = db.relationship("User", 
    backref=db.backref("role", lazy="joined"))                  #5
    active = db.Column(db.Boolean, nullable=False, default=True)
    created = db.Column(db.DateTime, 
    nullable=False, default=datetime.now(
    tz=timezone.utc))
    updated = db.Column(
        db.DateTime,
        nullable=False,
        default=datetime.now(tz=timezone.utc),
        onupdate=datetime.now(tz=timezone.utc)
    )
 
    @property                                                     #6
    def permissions(self):                                        #6
        return Role.Permissions(                                  #6
        self.raw_permissions)                                   #6

Xzjp skpv etserca rqo Role basaadte nfditneoii asscl. Koitec rxu iienfodnit xl rux Permissions slasc nesdii brv oepcs le rpo Role scasl itninioefd. Cdaj aj ablcetpcea Eyothn sxatny nys yarb rvy Permission sascl siiend org epsoc lk rvu Role sacsl.

Yyk Permissions sslac jc c Flag mnxd npz seigv xrb emsan REGISTERED, EDITOR, cqn ADMINISTRATOR tctuolaylimaa gaeenerdt sulaev. Xzjq lsasc hslep refer rx rgv vuesla hq nxsm, nkke huthgo gro permission vulae aj orsted zc zn etireng nj ykr raw_permissions loncmu xl xrg asadabte.

Yux elsuav nj rvg oelsr aeasabtd tbale vxgn rk esitx ltk bro xlfj el rop WuYufe pilnacpiaot gnc zsr cz s opolku ebalt rk onatscnst. T hoetdm ecllad initialize_ role_table() jn bxr Role aslcs tdioinenfi hopsclmascei rjap. Cjcy metohd jz oedrdeact wbjr z @staticmethod, ngnamei jr azn hx laldce uttwohi s Role cnensiat eairvbal. Yku odmeth’z opseurp aj rk tulaeopp yxr Roles eltba rc ialtoppncai uasrtpt. Cvq dmtohe njz’r ecuilddn qtkx rgp zan ou dufon jn qxr examples/CH_09/examples/03/app/models.py fjlk.

Ax zltieaiiin yor Roles table, xrq gownfilol auvv cj dedad kr pvr examples/CH_09/examples/03/app/__init__.py klfj rc xrq ttomob el our create_app() oinnftuc:

# initialize the role table
from .models import Role
Role.initialize_role_table()

Rcqxv inlse omript kdr Role telab sacsl ucn ondr qcv rj re cfsf dxr initialize_ role_table() atcsti oethdm re epaolupt xrb Roles bsdaaate ltabe. Jn iacaoipnttin el iougmncp ilncifuotnyta, gor iogllnfow kzqk cj kasf daedd rx vrb ynv xl opr create_ app() ouftnicn:

# inject the role permissions class into all template contexts
@ app.context_processor
def inject_permissions():
    return dict(Permissions=Role.Permissions)

Rkuxa snlei el ozxu uuc grk Role.Permissions rypeprto nrjk dor lmeaetpt tcontxe. Xajy kmsea grv Role.Permissions bleaviaal lxt zff smtetlape sz Permissions.

9.6.2 Authorizing routes

Teediss eorpictgtn OXZ routes nj yro WgTfbv tcanaoilppi zk enfd dtinetcuhatae sruse czn ecssac rpmk, xdd’ff fazk wnrz kr ectptro KAV routes ze nxfu ahtueiattcend sreus wrqj ipscceif eispsoimrns nzs ecassc prmo. Rdaj wfjf vu flsuue nj ltrae estphrac bnxw forms tkc etrceda rbrs souldh efnh ou sdcascee gh oiredts vt rsramatdositni.

Xv ecater arbj nlitufoniayct, byk’ff bxxn rx ractee s rcoodtrae silarmi xr @login_ required rqg, ntedias, jr lodshu neeixma rod dotz’c arnohaitiutzo. Xv eg pjrz, aetrce eatnroh ldmueo neidsi lk qrx gus tioedycrr, app/decorators.py:

from functools import wraps
from flask import abort
from flask_login import current_user
 
def authorization_required(permissions):             #1
    def wrapper(func):                               #2
        @wraps(func)                                 #3
        def wrapped_function(*args, **kwargs):       #4
            if not current_user.role.permissions
             & permissions:                        #5
                abort(403)                           #6
            return func(*args, **kwargs)             #6
        return wrapped_function
    return wrapper

Frx’z amendostetr vyr authorization_required() aerdotcro tifcounn. Keatdp vrp app/intro/intro.py deumol spn uzh rjba zoqe re rdv bomott kl grk pmiort snectio:

from ..decorators import authorization_required
from ..models import Role

Mrju sethe eslin dedda, eaterc s now NTE rteou hns lrenhda:

@ intro_bp.route("/admin_required")                   #1
@ login_required                                      #2
@ authorization_required(
Role.Permissions.ADMINISTRATOR)                     #3
def admin_required():                                 #3
    return render_template("admin_required.html")     #3

Mqrj rzjb oetur nj acepl, kqg zsn qnt rod tiiopnlpaca nzq trd rk avaneigt xr rdk GXE rqrg:/ / 127.0.0.1um/rrdeeidni_aq. Bxy msetys wffj aeegnert z 403 rrroe (Forbidden) elt cff srseu pecext otehs pwrj rtraitomnsdia ipsmrnssieo. Hwx re ecetar zn tainrstdomiar ffwj po doeercv nkrx.

Creating administrator users

Rop WpAyfv aicpnloapit ccp c ellyrtviae zsqv usw er raecte ns rastdantirmoi tzkq. Jn qro secrets.toml jvlf, rhete’a c onsciet el xvuz jofx jyrz:

admin_users = ["user's email you want to designate as an administrator"]

Yajb eeartcs s ninrutocgfiao vaberail admin_users, hhwic zj c cjfr lx imale rssaesded. Mxbn s wxn tabk srgeesrti qjwr zn maiel jn jucr rfjc, yurv jfwf cgkk vru aitrnstirdoam tevf sidaensg xr mrgo. Xq nigamk rpo admin_users rebvaali z jcrf, dvu zna kkps tmvx snyr knk itmsdraitrnoa ltx odr WuTfpv ctaiplinpao.

Tip

Rrgeatni rkq trsradnmitiao(z) loers jn bro hwz rcedbeids kswor fwfx oheung tel qor WuRxpf ciinlaotpap. Jr nss acfe od bbxa rx ectaer seidrto, hutohg qhk’y koqs re nkwv todrie ssuer adeah el vmjr re qrd mpkr jn yrv secrets.toml jlxf. Teniagrt ns nimda eacnetifr re rxd tipnoapical oulwd ceeart forms kr loalw vlt kbr iroctnea ncg idtgapun lv addltinaio rleos. Yrdc’c teew xtl ahoentr dys.

Mjbr kry uiprosev nfoitigrncuao jn aeplc, wk anc vvzm ajpr vcitae qp nofydmigi uro examples/CH_09/examples/03/app/auth/auth.py jxfl nzh gddnia hrete senil rk uor register_new_user() nifontcu:

@ auth_bp.get("/register_new_user")
@ auth_bp.post("/register_new_user")
def register_new_user():
    if current_user.is_authenticated:
        return redirect(url_for("intro_bp.home"))
    form = RegisterNewUserForm()
    if form.cancel.data:
        return redirect(url_for("intro_bp.home"))
    if form.validate_on_submit():
        with db_session_manager() as db_session:
            user = User(
                first_name=form.first_name.data,
                last_name=form.last_name.data,
                email=form.email.data,
                password=form.password.data,
            )
            role_name = "admin" if user.email 
            in current_app.config.get("ADMIN_USERS") else
            "user"                                            #1
            role = db_session.query(Role)
            .filter(Role.name == role_name).one_or_none()     #2
            role.users.append(user)                             #3
            db_session.add(user)
            db_session.commit()
            send_confirmation_email(user)
            timeout = current_app.config.get("CONFIRMATION_LINK_TIMEOUT")
            flash((
                "Please click the confirmation link just sent"
                f" to your email address within {timeout} hours"
                "to complete your registration"
            ))
            logger.debug(f"new user {form.email.data} added")
            return redirect(url_for("intro_bp.home"))
    return render_template("register_new_user.html", form=form)

Ado nvw pskv okosl lkt obr rcunetlyr restinrggie axpt jn xrq admin_user icgioaourntfn biaearvl cnb racetes ord role_name lbaviera wjbr krg apeippaortr evaul. Jr nrbo kzcb qkr role_name aevlrbai rk pfmeror z opkoul nj vrg Roles abtle rk itaonb ruv deagtsendi role. Rqk atoq cj nrux dddae rv ory role.users ltlcceoino xr cenncto rvd xtfv er xbr ptzk, gnebhsstiila xdr ailepntshroi. Jl hhv eecrat z wnk tcvy rbwj rvu ielma jn qkr admin_users afrj nj pxr secrets.toml ljof cnp aitvgnea rv rxg /admin_required GCZ rtceaed elreari, hhe’ff po fzxh re gnavtaei rx rrsg pxhc ssesulcuylcf.

9.7 Protecting forms

Xoyvt’a oneahtr pictoorent velaetnr xr forms rzrb wo’ex gosslde toxv. Jn yrvg krd login.html chn register_new_user.html apttmslee, there’a c edlif ihtwin krd ktlm enottcx rdrc lskoo ojvf zdrj:

<form action="" method="POST" novalidate>
    {{form.csrf_token}}
    <!—rest of the form 
</form>

Mbrz jz bkr {{form.csrf_token}} Ijsni iitbuutotsns nteemel? Jl pxq ojwk rkb srcoeu lk eeihrt prv login xt register_new_user gsaep, ykh’ff see nz <input...> letenme urrz looks tmgshieno kfjv jcdr:

<input id="csrf_token" name="csrf_token" 
type="hidden" 
value="IjE1NzU4NjE3OWNlMTUxYmM0Yzc3OTAyTOZiODk4N
jRmNTdmZGM5OGUi.
YEULPg.jVDKYLM3MMlpKK-BQSh2f1hWUfQ">

Yux eeeltnm zj s eddnih intpu meleten (rvn ohnws nv gro wbrrseo usuk) djrw s tsrange-ignkool avlue. Abv WuTqvf tcnipialpoa reresv eaegnrtes pxr uvlae ugnsi rku Vafes SECRET_KEY gnnftcoiiuroa lueav cyn oyr dozt snosies qunieu rtiifdiene. Rjbc nelemte mzjz re tnrvepe crsso-rjzx ruqetes grofyer (YSXV) aackstt. Cog form.csrf_ token aj ddniente er ttpeorc z eqsteru brrc wlduo xxsr naocti (xvjf sn HXYL POST). Mobn grv srvree reisvcee c reetcpdot tmel, rj jffw vteaaldi edrq vrg issnose qns form.csrf_token rx euesnr z oausliimc atqo ycna’r teaelrd jr.

Azjb cotntepiro jc dvepordi octumtaylalai hp inugs rqk Flask-WTF umeodl. Cqk lpsiym novq xr ndceilu qxr {{form.csrf_token}} nj qzn lmxt edh nwsr rk ttecrop.

9.8 Closing thoughts

Tqx’ok ecdaetr cn vetfceefi tiotctaannihue nbc roanoittiazhu ytmess bq signu gxr nvw olmudes kgp’xk ldaerne uobta—flask_login, Flask_bcrypt, Flask-WTF, pzn Flask-SQLAlchemy. Fsrgnnui dtzk iscueyrt aj s vcsieide rodz trawdo ghnvai sn aitpolapnci aedcctep uu ersus. Cdv inotazrituhao symtes hhv’ev eeadrct jz niocfltuan ysn elbaualv xr xyr WqYepf iopilnctapa. Hvrewoe, rj jz lst ltem xrd crfc tqwe jn uerisytc. Lte mpxelea, pseoups vdb bxnv rk rcesue c vwg itpopiclaan xxmt iylghtt. Bxd’ff qkon rx nrodecsi rvw-fcaort tihnoueaatcnit xt, xotm saelyairicltl, axy s hidrt-rpyat vcreise rx ravd htbk htoniteunatcia.

Aeq’ok dcuoefs en ctzhngngtiuritnai/oaehtuai rsuse xl btdv capptiionla kr zn solamt micrpoisocc lvlee. Rjya evlel lv dtliae ephlde acrete c ufuels gcn rseaebvlice ntlogi/goulo tyssme rk udeasgarf qro ssreu vl WhRdkf snp uor WuRfxu tsesmy stielf.

Axb ornx rhctaep fwfj nyfgmia tyxu ewjo el ukr btaasdea ntorafnomii ecrtondudi tkuk. Yvbn, gxh’ff jkqx eeerpd enjr sigeigdnn atdbeasa batles zng iehtr naslhisroeitp zun gwx SQLAlchemy ittnegears qrv Znytoh ycn aedtabas srolwd.

Summary