Clean up OAuth2 provider code, support multiple request types in preparation for OIDC
This commit is contained in:
parent
2a7aec1b46
commit
51fd91e3db
@ -2,151 +2,152 @@ import error from '../error'
|
||||
import response from '../response'
|
||||
import model from '../model'
|
||||
import authorization from './code'
|
||||
import wrap from '../../../../scripts/asyncRoute'
|
||||
|
||||
const usermodel = model.user
|
||||
import wrap from '../wrap'
|
||||
|
||||
module.exports = wrap(async (req, res, next) => {
|
||||
let clientId = null
|
||||
let redirectUri = null
|
||||
let responseType = null
|
||||
let grantType = null
|
||||
let grantTypes = []
|
||||
let scope = null
|
||||
let user = null
|
||||
|
||||
if (!req.query.redirect_uri) {
|
||||
return response.error(req, res, new error.InvalidRequest('redirect_uri field is mandatory for authorization endpoint'), redirectUri)
|
||||
throw new error.InvalidRequest('redirect_uri field is mandatory for authorization endpoint')
|
||||
}
|
||||
|
||||
redirectUri = req.query.redirect_uri
|
||||
console.debug('Parameter redirect uri is', redirectUri)
|
||||
|
||||
if (!req.query.client_id) {
|
||||
return response.error(req, res, new error.InvalidRequest('client_id field is mandatory for authorization endpoint'), redirectUri)
|
||||
throw new error.InvalidRequest('client_id field is mandatory for authorization endpoint')
|
||||
}
|
||||
|
||||
// Check for client_secret (prevent passing it)
|
||||
if (req.query.client_secret) {
|
||||
return response.error(req, res, new error.InvalidRequest('client_secret field should not be passed to the authorization endpoint'), redirectUri)
|
||||
throw new error.InvalidRequest('client_secret field should not be passed to the authorization endpoint')
|
||||
}
|
||||
|
||||
clientId = req.query.client_id
|
||||
console.debug('Parameter client_id is', clientId)
|
||||
|
||||
if (!req.query.response_type) {
|
||||
return response.error(req, res, new error.InvalidRequest('response_type field is mandatory for authorization endpoint'), redirectUri)
|
||||
throw new error.InvalidRequest('response_type field is mandatory for authorization endpoint')
|
||||
}
|
||||
|
||||
responseType = req.query.response_type
|
||||
console.debug('Parameter response_type is', responseType)
|
||||
|
||||
switch (responseType) {
|
||||
// Support multiple types
|
||||
const responseTypes = responseType.split(' ')
|
||||
for (const i in responseTypes) {
|
||||
switch (responseTypes[i]) {
|
||||
case 'code':
|
||||
grantType = 'authorization_code'
|
||||
grantTypes.push('authorization_code')
|
||||
break
|
||||
case 'token':
|
||||
grantType = 'implicit'
|
||||
grantTypes.push('implicit')
|
||||
break
|
||||
// case 'id_token':
|
||||
case 'none':
|
||||
grantTypes.push(responseTypes[i])
|
||||
break
|
||||
default:
|
||||
return response.error(req, res, new error.UnsupportedResponseType('Unknown response_type parameter passed'), redirectUri)
|
||||
throw new error.UnsupportedResponseType('Unknown response_type parameter passed')
|
||||
}
|
||||
console.debug('Parameter response_type is', responseType)
|
||||
}
|
||||
|
||||
// Filter out duplicates
|
||||
grantTypes = grantTypes.filter(function (value, index, self) {
|
||||
return self.indexOf(value) === index
|
||||
})
|
||||
|
||||
// "None" type cannot be combined with others
|
||||
if (grantTypes.length > 1 && grantTypes.indexOf('none') !== -1) {
|
||||
throw new error.InvalidRequest('Grant type "none" cannot be combined with other grant types')
|
||||
}
|
||||
|
||||
console.debug('Parameter grant_type is', grantTypes.join(' '))
|
||||
|
||||
const client = await req.oauth2.model.client.fetchById(clientId)
|
||||
if (!client) {
|
||||
return response.error(req, res, new error.InvalidClient('Client not found'), redirectUri)
|
||||
throw new error.InvalidClient('Client not found')
|
||||
}
|
||||
|
||||
// TODO: multiple redirect URI
|
||||
if (!req.oauth2.model.client.getRedirectUri(client)) {
|
||||
return response.error(req, res, new error.UnsupportedResponseType('The client has not set a redirect uri'), redirectUri)
|
||||
throw new error.UnsupportedResponseType('The client has not set a redirect uri')
|
||||
} else if (!req.oauth2.model.client.checkRedirectUri(client, redirectUri)) {
|
||||
return response.error(req, res, new error.InvalidRequest('Wrong RedirectUri provided'), redirectUri)
|
||||
} else {
|
||||
throw new error.InvalidRequest('Wrong RedirectUri provided')
|
||||
}
|
||||
console.debug('redirect_uri check passed')
|
||||
}
|
||||
|
||||
if (!req.oauth2.model.client.checkGrantType(client, grantType)) {
|
||||
return response.error(req, res, new error.UnauthorizedClient('This client does not support this grant type'), redirectUri)
|
||||
} else {
|
||||
console.debug('Grant type check passed')
|
||||
// The client needs to support all grant types
|
||||
for (const i in grantTypes) {
|
||||
if (!req.oauth2.model.client.checkGrantType(client, grantTypes[i]) && grantTypes[i] !== 'none') {
|
||||
throw new error.UnauthorizedClient('This client does not support grant type ' + grantTypes[i])
|
||||
}
|
||||
}
|
||||
console.debug('Grant type check passed')
|
||||
|
||||
scope = req.oauth2.model.client.transformScope(req.query.scope)
|
||||
scope = req.oauth2.model.client.checkScope(client, scope)
|
||||
if (!scope) {
|
||||
return response.error(req, res, new error.InvalidScope('Client does not allow access to this scope'), redirectUri)
|
||||
} else {
|
||||
console.debug('Scope check passed')
|
||||
throw new error.InvalidScope('Client does not allow access to this scope')
|
||||
}
|
||||
console.debug('Scope check passed')
|
||||
|
||||
user = await req.oauth2.model.user.fetchFromRequest(req)
|
||||
if (!user) {
|
||||
return response.error(req, res, new error.InvalidRequest('There is no currently logged in user'), redirectUri)
|
||||
throw new error.InvalidRequest('There is no currently logged in user')
|
||||
} else {
|
||||
if (!user.username) {
|
||||
return response.error(req, res, new error.Forbidden(user), redirectUri)
|
||||
throw new error.Forbidden(user)
|
||||
}
|
||||
console.debug('User fetched from request')
|
||||
}
|
||||
|
||||
let data = null
|
||||
let resObj = {}
|
||||
let consented = false
|
||||
|
||||
if (req.method === 'GET') {
|
||||
let hasAuthorizedAlready = await usermodel.clientAllowed(user.id, client.id, scope)
|
||||
if (client.verified === 1) {
|
||||
hasAuthorizedAlready = true
|
||||
consented = true
|
||||
} else {
|
||||
consented = await model.user.consented(user.id, client.id, scope)
|
||||
}
|
||||
}
|
||||
|
||||
if (hasAuthorizedAlready) {
|
||||
if (grantType === 'authorization_code') {
|
||||
try {
|
||||
data = await authorization.Code(req, res, client, scope, user, redirectUri, false)
|
||||
} catch (err) {
|
||||
return response.error(req, res, err, redirectUri)
|
||||
}
|
||||
// Ask for consent
|
||||
if (!consented) return req.oauth2.decision(req, res, client, scope, user, redirectUri)
|
||||
|
||||
return response.data(req, res, { code: data }, redirectUri)
|
||||
} else if (grantType === 'implicit') {
|
||||
try {
|
||||
data = await authorization.Implicit(req, res, client, scope, user, redirectUri, false)
|
||||
} catch (err) {
|
||||
return response.error(req, res, err, redirectUri)
|
||||
}
|
||||
for (const i in grantTypes) {
|
||||
let data = null
|
||||
switch (grantTypes[i]) {
|
||||
case 'authorization_code':
|
||||
data = await authorization.Code(req, res, client, scope, user, redirectUri, !consented)
|
||||
|
||||
return response.data(req, res, {
|
||||
resObj = Object.assign({ code: data }, resObj)
|
||||
|
||||
break
|
||||
case 'implicit':
|
||||
data = await authorization.Implicit(req, res, client, scope, user, redirectUri, !consented)
|
||||
|
||||
resObj = Object.assign({
|
||||
token_type: 'bearer',
|
||||
access_token: data,
|
||||
expires_in: req.oauth2.model.accessToken.ttl
|
||||
}, redirectUri)
|
||||
}, resObj)
|
||||
|
||||
break
|
||||
case 'none':
|
||||
resObj = {}
|
||||
break
|
||||
default:
|
||||
throw new error.UnsupportedResponseType('Unknown response_type parameter passed')
|
||||
}
|
||||
} else {
|
||||
return req.oauth2.decision(req, res, client, scope, user, redirectUri)
|
||||
}
|
||||
|
||||
return response.error(req, res, new error.InvalidRequest('Invalid request method'), redirectUri)
|
||||
}
|
||||
|
||||
if (grantType === 'authorization_code') {
|
||||
try {
|
||||
data = await authorization.Code(req, res, client, scope, user, redirectUri, true)
|
||||
} catch (err) {
|
||||
return response.error(req, res, err, redirectUri)
|
||||
}
|
||||
|
||||
return response.data(req, res, { code: data }, redirectUri)
|
||||
} else if (grantType === 'implicit') {
|
||||
try {
|
||||
data = await authorization.Implicit(req, res, client, scope, user, redirectUri, true)
|
||||
} catch (err) {
|
||||
return response.error(req, res, err, redirectUri)
|
||||
}
|
||||
|
||||
return response.data(req, res, {
|
||||
token_type: 'bearer',
|
||||
access_token: data,
|
||||
expires_in: req.oauth2.model.accessToken.ttl
|
||||
}, redirectUri)
|
||||
} else {
|
||||
return response.error(req, res, new error.InvalidRequest('Invalid request method'), redirectUri)
|
||||
}
|
||||
})
|
||||
// Return non-code response types as fragment instead of query
|
||||
return response.data(req, res, resObj, redirectUri, responseType !== 'code')
|
||||
}, true)
|
||||
|
@ -1,23 +1,22 @@
|
||||
import error from '../../error'
|
||||
import model from '../../model'
|
||||
|
||||
module.exports = async (req, res, client, scope, user, redirectUri, createAllowFuture) => {
|
||||
module.exports = async (req, res, client, scope, user, redirectUri, consentRequested) => {
|
||||
let codeValue = null
|
||||
|
||||
if (req.method === 'POST' && req.session.csrf && !(req.body.csrf && req.body.csrf === req.session.csrf)) {
|
||||
throw new error.InvalidRequest('Invalid session')
|
||||
}
|
||||
|
||||
if (createAllowFuture) {
|
||||
if (consentRequested) {
|
||||
if (!req.body || (typeof req.body.decision) === 'undefined') {
|
||||
throw new error.InvalidRequest('No decision parameter passed')
|
||||
} else if (req.body.decision === '0') {
|
||||
throw new error.AccessDenied('User denied access to the resource')
|
||||
} else {
|
||||
console.debug('Decision check passed')
|
||||
}
|
||||
console.debug('Decision check passed')
|
||||
|
||||
await model.user.allowClient(user.id, client.id, scope)
|
||||
await model.user.consent(user.id, client.id, scope)
|
||||
}
|
||||
|
||||
try {
|
||||
|
@ -1,23 +1,22 @@
|
||||
import error from '../../error'
|
||||
import model from '../../model'
|
||||
|
||||
module.exports = async (req, res, client, scope, user, redirectUri, createAllowFuture) => {
|
||||
module.exports = async (req, res, client, scope, user, redirectUri, consentRequested) => {
|
||||
let accessTokenValue = null
|
||||
|
||||
if (req.method === 'POST' && req.session.csrf && !(req.body.csrf && req.body.csrf === req.session.csrf)) {
|
||||
throw new error.InvalidRequest('Invalid session')
|
||||
}
|
||||
|
||||
if (createAllowFuture) {
|
||||
if (consentRequested) {
|
||||
if (!req.body || (typeof req.body.decision) === 'undefined') {
|
||||
throw new error.InvalidRequest('No decision parameter passed')
|
||||
} else if (req.body.decision === '0') {
|
||||
throw new error.AccessDenied('User denied access to the resource')
|
||||
} else {
|
||||
console.debug('Decision check passed')
|
||||
}
|
||||
console.debug('Decision check passed')
|
||||
|
||||
await model.user.allowClient(user.id, client.id, scope)
|
||||
await model.user.consent(user.id, client.id, scope)
|
||||
}
|
||||
|
||||
try {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import error from '../error'
|
||||
import response from '../response'
|
||||
import wrap from '../../../../scripts/asyncRoute'
|
||||
import wrap from '../wrap'
|
||||
|
||||
module.exports = wrap(async function (req, res) {
|
||||
let clientId = null
|
||||
@ -12,21 +12,21 @@ module.exports = wrap(async function (req, res) {
|
||||
console.debug('Client credentials parsed from body parameters ', clientId, clientSecret)
|
||||
} else {
|
||||
if (!req.headers || !req.headers.authorization) {
|
||||
return response.error(req, res, new error.InvalidRequest('No authorization header passed'))
|
||||
throw new error.InvalidRequest('No authorization header passed')
|
||||
}
|
||||
|
||||
let pieces = req.headers.authorization.split(' ', 2)
|
||||
if (!pieces || pieces.length !== 2) {
|
||||
return response.error(req, res, new error.InvalidRequest('Authorization header is corrupted'))
|
||||
throw new error.InvalidRequest('Authorization header is corrupted')
|
||||
}
|
||||
|
||||
if (pieces[0] !== 'Basic') {
|
||||
return response.error(req, res, new error.InvalidRequest('Unsupported authorization method:', pieces[0]))
|
||||
throw new error.InvalidRequest('Unsupported authorization method:', pieces[0])
|
||||
}
|
||||
|
||||
pieces = Buffer.from(pieces[1], 'base64').toString('ascii').split(':', 2)
|
||||
if (!pieces || pieces.length !== 2) {
|
||||
return response.error(req, res, new error.InvalidRequest('Authorization header has corrupted data'))
|
||||
throw new error.InvalidRequest('Authorization header has corrupted data')
|
||||
}
|
||||
|
||||
clientId = pieces[0]
|
||||
@ -35,12 +35,12 @@ module.exports = wrap(async function (req, res) {
|
||||
}
|
||||
|
||||
if (!req.body.token) {
|
||||
return response.error(req, res, new error.InvalidRequest('Token not provided in request body'))
|
||||
throw new error.InvalidRequest('Token not provided in request body')
|
||||
}
|
||||
|
||||
const token = await req.oauth2.model.accessToken.fetchByToken(req.body.token)
|
||||
if (!token) {
|
||||
return response.error(req, res, new error.InvalidRequest('Token does not exist'))
|
||||
throw new error.InvalidRequest('Token does not exist')
|
||||
}
|
||||
|
||||
const ttl = req.oauth2.model.accessToken.getTTL(token)
|
||||
|
@ -1,7 +1,7 @@
|
||||
import token from './tokens'
|
||||
import error from '../error'
|
||||
import response from '../response'
|
||||
import wrap from '../../../../scripts/asyncRoute'
|
||||
import wrap from '../wrap'
|
||||
|
||||
module.exports = wrap(async (req, res) => {
|
||||
let clientId = null
|
||||
@ -14,21 +14,21 @@ module.exports = wrap(async (req, res) => {
|
||||
console.debug('Client credentials parsed from body parameters', clientId, clientSecret)
|
||||
} else {
|
||||
if (!req.headers || !req.headers.authorization) {
|
||||
return response.error(req, res, new error.InvalidRequest('No authorization header passed'))
|
||||
throw new error.InvalidRequest('No authorization header passed')
|
||||
}
|
||||
|
||||
let pieces = req.headers.authorization.split(' ', 2)
|
||||
if (!pieces || pieces.length !== 2) {
|
||||
return response.error(req, res, new error.InvalidRequest('Authorization header is corrupted'))
|
||||
throw new error.InvalidRequest('Authorization header is corrupted')
|
||||
}
|
||||
|
||||
if (pieces[0] !== 'Basic') {
|
||||
return response.error(req, res, new error.InvalidRequest('Unsupported authorization method:', pieces[0]))
|
||||
throw new error.InvalidRequest('Unsupported authorization method:', pieces[0])
|
||||
}
|
||||
|
||||
pieces = Buffer.from(pieces[1], 'base64').toString('ascii').split(':', 2)
|
||||
if (!pieces || pieces.length !== 2) {
|
||||
return response.error(req, res, new error.InvalidRequest('Authorization header has corrupted data'))
|
||||
throw new error.InvalidRequest('Authorization header has corrupted data')
|
||||
}
|
||||
|
||||
clientId = pieces[0]
|
||||
@ -37,7 +37,7 @@ module.exports = wrap(async (req, res) => {
|
||||
}
|
||||
|
||||
if (!req.body.grant_type) {
|
||||
return response.error(req, res, new error.InvalidRequest('Request body does not contain grant_type parameter'))
|
||||
throw new error.InvalidRequest('Request body does not contain grant_type parameter')
|
||||
}
|
||||
|
||||
grantType = req.body.grant_type
|
||||
@ -46,16 +46,16 @@ module.exports = wrap(async (req, res) => {
|
||||
const client = await req.oauth2.model.client.fetchById(clientId)
|
||||
|
||||
if (!client) {
|
||||
return response.error(req, res, new error.InvalidClient('Client not found'))
|
||||
throw new error.InvalidClient('Client not found')
|
||||
}
|
||||
|
||||
const valid = req.oauth2.model.client.checkSecret(client, clientSecret)
|
||||
if (!valid) {
|
||||
return response.error(req, res, new error.UnauthorizedClient('Invalid client secret'))
|
||||
throw new error.UnauthorizedClient('Invalid client secret')
|
||||
}
|
||||
|
||||
if (!req.oauth2.model.client.checkGrantType(client, grantType) && grantType !== 'refresh_token') {
|
||||
return response.error(req, res, new error.UnauthorizedClient('Invalid grant type for the client'))
|
||||
throw new error.UnauthorizedClient('Invalid grant type for the client')
|
||||
} else {
|
||||
console.debug('Grant type check passed')
|
||||
}
|
||||
|
@ -12,10 +12,10 @@ module.exports = async (oauth2, client, wantScope) => {
|
||||
|
||||
if (!scope) {
|
||||
throw new error.InvalidScope('Client does not allow access to this scope')
|
||||
} else {
|
||||
console.debug('Scope check passed ', scope)
|
||||
}
|
||||
|
||||
console.debug('Scope check passed ', scope)
|
||||
|
||||
try {
|
||||
resObj.access_token = await oauth2.model.accessToken.create(null, oauth2.model.client.getId(client), scope, oauth2.model.accessToken.ttl)
|
||||
} catch (err) {
|
||||
|
@ -1,6 +1,5 @@
|
||||
import response from './response'
|
||||
import error from './error'
|
||||
import wrap from '../../../scripts/asyncRoute'
|
||||
import wrap from './wrap'
|
||||
|
||||
const middleware = wrap(async function (req, res, next) {
|
||||
console.debug('Parsing bearer token')
|
||||
@ -12,12 +11,12 @@ const middleware = wrap(async function (req, res, next) {
|
||||
|
||||
// Check authorization header
|
||||
if (!pieces || pieces.length !== 2) {
|
||||
return response.error(req, res, new error.AccessDenied('Wrong authorization header'))
|
||||
throw new error.AccessDenied('Wrong authorization header')
|
||||
}
|
||||
|
||||
// Only bearer auth is supported
|
||||
if (pieces[0].toLowerCase() !== 'bearer') {
|
||||
return response.error(req, res, new error.AccessDenied('Unsupported authorization method in header'))
|
||||
throw new error.AccessDenied('Unsupported authorization method in header')
|
||||
}
|
||||
|
||||
token = pieces[1]
|
||||
@ -29,15 +28,15 @@ const middleware = wrap(async function (req, res, next) {
|
||||
token = req.body.access_token
|
||||
console.debug('Bearer token parsed from body params:', token)
|
||||
} else {
|
||||
return response.error(req, res, new error.AccessDenied('Bearer token not found'))
|
||||
throw new error.AccessDenied('Bearer token not found')
|
||||
}
|
||||
|
||||
// Try to fetch access token
|
||||
const object = await req.oauth2.model.accessToken.fetchByToken(token)
|
||||
if (!object) {
|
||||
response.error(req, res, new error.Forbidden('Token not found or has expired'))
|
||||
throw new error.Forbidden('Token not found or has expired')
|
||||
} else if (!req.oauth2.model.accessToken.checkTTL(object)) {
|
||||
response.error(req, res, new error.Forbidden('Token is expired'))
|
||||
throw new error.Forbidden('Token is expired')
|
||||
} else {
|
||||
req.oauth2.accessToken = object
|
||||
console.debug('AccessToken fetched', object)
|
||||
|
@ -4,6 +4,11 @@ import Users from '../index'
|
||||
import crypto from 'crypto'
|
||||
|
||||
const OAuthDB = {
|
||||
scopes: {
|
||||
email: ['See your Email address', 'View the user\'s email address'],
|
||||
image: ['', 'View the user\'s profile picture'],
|
||||
privilege: ['', 'See the user\'s privilege level']
|
||||
},
|
||||
accessToken: {
|
||||
ttl: config.oauth2.access_token_life,
|
||||
getToken: (object) => {
|
||||
@ -237,7 +242,7 @@ const OAuthDB = {
|
||||
|
||||
return req.session.user
|
||||
},
|
||||
clientAllowed: async (userId, clientId, scope) => {
|
||||
consented: async (userId, clientId, scope) => {
|
||||
if (typeof scope === 'object') {
|
||||
scope = scope.join(' ')
|
||||
}
|
||||
@ -265,7 +270,7 @@ const OAuthDB = {
|
||||
|
||||
return correct
|
||||
},
|
||||
allowClient: async (userId, clientId, scope) => {
|
||||
consent: async (userId, clientId, scope) => {
|
||||
if (!config.oauth2.save_decision) return true
|
||||
if (typeof scope === 'object') {
|
||||
scope = scope.join(' ')
|
||||
|
@ -40,9 +40,9 @@ module.exports.error = function (req, res, err, redirectUri) {
|
||||
}
|
||||
}
|
||||
|
||||
module.exports.data = function (req, res, obj, redirectUri, anchor) {
|
||||
module.exports.data = function (req, res, obj, redirectUri, fragment) {
|
||||
if (redirectUri) {
|
||||
if (anchor) {
|
||||
if (fragment) {
|
||||
redirectUri += '#'
|
||||
} else {
|
||||
redirectUri += (redirectUri.indexOf('?') === -1 ? '?' : '&')
|
||||
|
9
server/api/oauth2/wrap.js
Normal file
9
server/api/oauth2/wrap.js
Normal file
@ -0,0 +1,9 @@
|
||||
import response from './response'
|
||||
|
||||
module.exports = function (fn, redir) {
|
||||
return function (req, res, next) {
|
||||
fn(req, res, next).catch(e => {
|
||||
return response.error(req, res, e, redir ? req.query.redirect_uri : null)
|
||||
})
|
||||
}
|
||||
}
|
103
static/image/icynet-icon-pleroma.svg
Normal file
103
static/image/icynet-icon-pleroma.svg
Normal file
@ -0,0 +1,103 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="29.898235mm"
|
||||
height="29.898235mm"
|
||||
viewBox="0 0 29.898235 29.898235"
|
||||
version="1.1"
|
||||
id="svg8">
|
||||
<defs
|
||||
id="defs2">
|
||||
<clipPath
|
||||
clipPathUnits="userSpaceOnUse"
|
||||
id="clipPath877">
|
||||
<rect
|
||||
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#6fefff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
|
||||
id="rect879"
|
||||
width="24.856066"
|
||||
height="24.856066"
|
||||
x="2.5211489"
|
||||
y="2.5227857"
|
||||
transform="rotate(2.4309033e-4)" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
<metadata
|
||||
id="metadata5">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
id="layer1"
|
||||
transform="translate(-1.2029979e-7,-0.00176382)">
|
||||
<rect
|
||||
transform="rotate(-3.5293181)"
|
||||
y="3.4147503"
|
||||
x="1.5723678"
|
||||
height="24.856066"
|
||||
width="24.856066"
|
||||
id="rect1083"
|
||||
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#00a1b5;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate;font-variant-east_asian:normal" />
|
||||
<rect
|
||||
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#00e1fd;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate;font-variant-east_asian:normal"
|
||||
id="rect815"
|
||||
width="24.856066"
|
||||
height="24.856066"
|
||||
x="0.13787289"
|
||||
y="4.5749965"
|
||||
transform="rotate(-8.5307657)" />
|
||||
<rect
|
||||
transform="rotate(2.4309033e-4)"
|
||||
y="2.5227857"
|
||||
x="2.5211489"
|
||||
height="24.856066"
|
||||
width="24.856066"
|
||||
id="rect817"
|
||||
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#6fefff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
|
||||
<g
|
||||
transform="matrix(0.06114614,0,0,0.06114614,-0.73486692,-0.70252952)"
|
||||
id="g4612"
|
||||
style="font-variation-settings:normal;opacity:1;vector-effect:none;fill:#03a9f4;fill-opacity:1;stroke:none;stroke-width:4.88991;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;stop-color:#000000;stop-opacity:1">
|
||||
<path
|
||||
style="font-variation-settings:normal;opacity:1;vector-effect:none;fill:#03a9f4;fill-opacity:1;stroke:none;stroke-width:4.88991;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;stop-color:#000000;stop-opacity:1"
|
||||
d="M 235,89 V 423 H 152 V 115 l 26,-26 z"
|
||||
id="path4495" />
|
||||
<circle
|
||||
style="font-variation-settings:normal;opacity:1;vector-effect:none;fill:#03a9f4;fill-opacity:1;stroke:none;stroke-width:4.88991;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;stop-color:#000000;stop-opacity:1"
|
||||
id="path4497"
|
||||
cy="115"
|
||||
cx="178"
|
||||
r="26" />
|
||||
<circle
|
||||
style="font-variation-settings:normal;opacity:1;vector-effect:none;fill:#03a9f4;fill-opacity:1;stroke:none;stroke-width:4.88991;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;stop-color:#000000;stop-opacity:1"
|
||||
id="path4497-0"
|
||||
cy="230"
|
||||
cx="335"
|
||||
r="26" />
|
||||
<path
|
||||
style="font-variation-settings:normal;vector-effect:none;fill:#03a9f4;fill-opacity:1;stroke:none;stroke-width:4.88991;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;stop-color:#000000;stop-opacity:1"
|
||||
d="M 277,256 V 89 l 84,3e-6 L 361.00002,230 335,256 Z"
|
||||
id="path4516" />
|
||||
<circle
|
||||
style="font-variation-settings:normal;opacity:1;vector-effect:none;fill:#03a9f4;fill-opacity:1;stroke:none;stroke-width:4.88991;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;stop-color:#000000;stop-opacity:1"
|
||||
id="path4497-0-6"
|
||||
cy="397"
|
||||
cx="335"
|
||||
r="26" />
|
||||
<path
|
||||
style="font-variation-settings:normal;opacity:1;vector-effect:none;fill:#03a9f4;fill-opacity:1;stroke:none;stroke-width:4.88991;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;stop-color:#000000;stop-opacity:1"
|
||||
d="m 277,423 v -83 h 84 l 2e-5,57 L 335,423 Z"
|
||||
id="path4516-5" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 6.6 KiB |
@ -54,7 +54,7 @@ block body
|
||||
i.fas.fa-fw.fa-lock
|
||||
|See your Password
|
||||
li
|
||||
i.fas.fa-fw.fa-gears
|
||||
i.fas.fa-fw.fa-cogs
|
||||
|Change your Account Settings
|
||||
.alert.alert-info
|
||||
b Note!
|
||||
|
Reference in New Issue
Block a user