Page Menu
Home
c4science
Search
Configure Global Search
Log In
Files
F119227042
PhabricatorAuthFactor.php
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Subscribers
None
File Metadata
Details
File Info
Storage
Attached
Created
Wed, Jun 25, 12:30
Size
16 KB
Mime Type
text/x-php
Expires
Fri, Jun 27, 12:30 (1 d, 23 h)
Engine
blob
Format
Raw Data
Handle
26995423
Attached To
rPH Phabricator
PhabricatorAuthFactor.php
View Options
<?php
abstract
class
PhabricatorAuthFactor
extends
Phobject
{
abstract
public
function
getFactorName
();
abstract
public
function
getFactorShortName
();
abstract
public
function
getFactorKey
();
abstract
public
function
getFactorCreateHelp
();
abstract
public
function
getFactorDescription
();
abstract
public
function
processAddFactorForm
(
PhabricatorAuthFactorProvider
$provider
,
AphrontFormView
$form
,
AphrontRequest
$request
,
PhabricatorUser
$user
);
abstract
public
function
renderValidateFactorForm
(
PhabricatorAuthFactorConfig
$config
,
AphrontFormView
$form
,
PhabricatorUser
$viewer
,
PhabricatorAuthFactorResult
$validation_result
);
public
function
getParameterName
(
PhabricatorAuthFactorConfig
$config
,
$name
)
{
return
'authfactor.'
.
$config
->
getID
().
'.'
.
$name
;
}
public
static
function
getAllFactors
()
{
return
id
(
new
PhutilClassMapQuery
())
->
setAncestorClass
(
__CLASS__
)
->
setUniqueMethod
(
'getFactorKey'
)
->
execute
();
}
protected
function
newConfigForUser
(
PhabricatorUser
$user
)
{
return
id
(
new
PhabricatorAuthFactorConfig
())
->
setUserPHID
(
$user
->
getPHID
())
->
setFactorSecret
(
''
);
}
protected
function
newResult
()
{
return
new
PhabricatorAuthFactorResult
();
}
public
function
newIconView
()
{
return
id
(
new
PHUIIconView
())
->
setIcon
(
'fa-mobile'
);
}
public
function
canCreateNewProvider
()
{
return
true
;
}
public
function
getProviderCreateDescription
()
{
return
null
;
}
public
function
canCreateNewConfiguration
(
PhabricatorAuthFactorProvider
$provider
,
PhabricatorUser
$user
)
{
return
true
;
}
public
function
getConfigurationCreateDescription
(
PhabricatorAuthFactorProvider
$provider
,
PhabricatorUser
$user
)
{
return
null
;
}
public
function
getConfigurationListDetails
(
PhabricatorAuthFactorConfig
$config
,
PhabricatorAuthFactorProvider
$provider
,
PhabricatorUser
$viewer
)
{
return
null
;
}
public
function
newEditEngineFields
(
PhabricatorEditEngine
$engine
,
PhabricatorAuthFactorProvider
$provider
)
{
return
array
();
}
public
function
newChallengeStatusView
(
PhabricatorAuthFactorConfig
$config
,
PhabricatorAuthFactorProvider
$provider
,
PhabricatorUser
$viewer
,
PhabricatorAuthChallenge
$challenge
)
{
return
null
;
}
/**
* Is this a factor which depends on the user's contact number?
*
* If a user has a "contact number" factor configured, they can not modify
* or switch their primary contact number.
*
* @return bool True if this factor should lock contact numbers.
*/
public
function
isContactNumberFactor
()
{
return
false
;
}
abstract
public
function
getEnrollDescription
(
PhabricatorAuthFactorProvider
$provider
,
PhabricatorUser
$user
);
public
function
getEnrollButtonText
(
PhabricatorAuthFactorProvider
$provider
,
PhabricatorUser
$user
)
{
return
pht
(
'Continue'
);
}
public
function
getFactorOrder
()
{
return
1000
;
}
final
public
function
newSortVector
()
{
return
id
(
new
PhutilSortVector
())
->
addInt
(
$this
->
canCreateNewProvider
()
?
0
:
1
)
->
addInt
(
$this
->
getFactorOrder
())
->
addString
(
$this
->
getFactorName
());
}
protected
function
newChallenge
(
PhabricatorAuthFactorConfig
$config
,
PhabricatorUser
$viewer
)
{
$engine
=
$config
->
getSessionEngine
();
return
PhabricatorAuthChallenge
::
initializeNewChallenge
()
->
setUserPHID
(
$viewer
->
getPHID
())
->
setSessionPHID
(
$viewer
->
getSession
()->
getPHID
())
->
setFactorPHID
(
$config
->
getPHID
())
->
setIsNewChallenge
(
true
)
->
setWorkflowKey
(
$engine
->
getWorkflowKey
());
}
abstract
public
function
getRequestHasChallengeResponse
(
PhabricatorAuthFactorConfig
$config
,
AphrontRequest
$response
);
final
public
function
getNewIssuedChallenges
(
PhabricatorAuthFactorConfig
$config
,
PhabricatorUser
$viewer
,
array
$challenges
)
{
assert_instances_of
(
$challenges
,
'PhabricatorAuthChallenge'
);
$now
=
PhabricatorTime
::
getNow
();
// Factor implementations may need to perform writes in order to issue
// challenges, particularly push factors like SMS.
$unguarded
=
AphrontWriteGuard
::
beginScopedUnguardedWrites
();
$new_challenges
=
$this
->
newIssuedChallenges
(
$config
,
$viewer
,
$challenges
);
if
(
$this
->
isAuthResult
(
$new_challenges
))
{
unset
(
$unguarded
);
return
$new_challenges
;
}
assert_instances_of
(
$new_challenges
,
'PhabricatorAuthChallenge'
);
foreach
(
$new_challenges
as
$new_challenge
)
{
$ttl
=
$new_challenge
->
getChallengeTTL
();
if
(!
$ttl
)
{
throw
new
Exception
(
pht
(
'Newly issued MFA challenges must have a valid TTL!'
));
}
if
(
$ttl
<
$now
)
{
throw
new
Exception
(
pht
(
'Newly issued MFA challenges must have a future TTL. This '
.
'factor issued a bad TTL ("%s"). (Did you use a relative '
.
'time instead of an epoch?)'
,
$ttl
));
}
}
foreach
(
$new_challenges
as
$challenge
)
{
$challenge
->
save
();
}
unset
(
$unguarded
);
return
$new_challenges
;
}
abstract
protected
function
newIssuedChallenges
(
PhabricatorAuthFactorConfig
$config
,
PhabricatorUser
$viewer
,
array
$challenges
);
final
public
function
getResultFromIssuedChallenges
(
PhabricatorAuthFactorConfig
$config
,
PhabricatorUser
$viewer
,
array
$challenges
)
{
assert_instances_of
(
$challenges
,
'PhabricatorAuthChallenge'
);
$result
=
$this
->
newResultFromIssuedChallenges
(
$config
,
$viewer
,
$challenges
);
if
(
$result
===
null
)
{
return
$result
;
}
if
(!
$this
->
isAuthResult
(
$result
))
{
throw
new
Exception
(
pht
(
'Expected "newResultFromIssuedChallenges()" to return null or '
.
'an object of class "%s"; got something else (in "%s").'
,
'PhabricatorAuthFactorResult'
,
get_class
(
$this
)));
}
return
$result
;
}
final
public
function
getResultForPrompt
(
PhabricatorAuthFactorConfig
$config
,
PhabricatorUser
$viewer
,
AphrontRequest
$request
,
array
$challenges
)
{
assert_instances_of
(
$challenges
,
'PhabricatorAuthChallenge'
);
$result
=
$this
->
newResultForPrompt
(
$config
,
$viewer
,
$request
,
$challenges
);
if
(!
$this
->
isAuthResult
(
$result
))
{
throw
new
Exception
(
pht
(
'Expected "newResultForPrompt()" to return an object of class "%s", '
.
'but it returned something else ("%s"; in "%s").'
,
'PhabricatorAuthFactorResult'
,
phutil_describe_type
(
$result
),
get_class
(
$this
)));
}
return
$result
;
}
protected
function
newResultForPrompt
(
PhabricatorAuthFactorConfig
$config
,
PhabricatorUser
$viewer
,
AphrontRequest
$request
,
array
$challenges
)
{
return
$this
->
newResult
();
}
abstract
protected
function
newResultFromIssuedChallenges
(
PhabricatorAuthFactorConfig
$config
,
PhabricatorUser
$viewer
,
array
$challenges
);
final
public
function
getResultFromChallengeResponse
(
PhabricatorAuthFactorConfig
$config
,
PhabricatorUser
$viewer
,
AphrontRequest
$request
,
array
$challenges
)
{
assert_instances_of
(
$challenges
,
'PhabricatorAuthChallenge'
);
$result
=
$this
->
newResultFromChallengeResponse
(
$config
,
$viewer
,
$request
,
$challenges
);
if
(!
$this
->
isAuthResult
(
$result
))
{
throw
new
Exception
(
pht
(
'Expected "newResultFromChallengeResponse()" to return an object '
.
'of class "%s"; got something else (in "%s").'
,
'PhabricatorAuthFactorResult'
,
get_class
(
$this
)));
}
return
$result
;
}
abstract
protected
function
newResultFromChallengeResponse
(
PhabricatorAuthFactorConfig
$config
,
PhabricatorUser
$viewer
,
AphrontRequest
$request
,
array
$challenges
);
final
protected
function
newAutomaticControl
(
PhabricatorAuthFactorResult
$result
)
{
$is_error
=
$result
->
getIsError
();
if
(
$is_error
)
{
return
$this
->
newErrorControl
(
$result
);
}
$is_continue
=
$result
->
getIsContinue
();
if
(
$is_continue
)
{
return
$this
->
newContinueControl
(
$result
);
}
$is_answered
=
(
bool
)
$result
->
getAnsweredChallenge
();
if
(
$is_answered
)
{
return
$this
->
newAnsweredControl
(
$result
);
}
$is_wait
=
$result
->
getIsWait
();
if
(
$is_wait
)
{
return
$this
->
newWaitControl
(
$result
);
}
return
null
;
}
private
function
newWaitControl
(
PhabricatorAuthFactorResult
$result
)
{
$error
=
$result
->
getErrorMessage
();
$icon
=
$result
->
getIcon
();
if
(!
$icon
)
{
$icon
=
id
(
new
PHUIIconView
())
->
setIcon
(
'fa-clock-o'
,
'red'
);
}
return
id
(
new
PHUIFormTimerControl
())
->
setIcon
(
$icon
)
->
appendChild
(
$error
)
->
setError
(
pht
(
'Wait'
));
}
private
function
newAnsweredControl
(
PhabricatorAuthFactorResult
$result
)
{
$icon
=
$result
->
getIcon
();
if
(!
$icon
)
{
$icon
=
id
(
new
PHUIIconView
())
->
setIcon
(
'fa-check-circle-o'
,
'green'
);
}
return
id
(
new
PHUIFormTimerControl
())
->
setIcon
(
$icon
)
->
appendChild
(
pht
(
'You responded to this challenge correctly.'
));
}
private
function
newErrorControl
(
PhabricatorAuthFactorResult
$result
)
{
$error
=
$result
->
getErrorMessage
();
$icon
=
$result
->
getIcon
();
if
(!
$icon
)
{
$icon
=
id
(
new
PHUIIconView
())
->
setIcon
(
'fa-times'
,
'red'
);
}
return
id
(
new
PHUIFormTimerControl
())
->
setIcon
(
$icon
)
->
appendChild
(
$error
)
->
setError
(
pht
(
'Error'
));
}
private
function
newContinueControl
(
PhabricatorAuthFactorResult
$result
)
{
$error
=
$result
->
getErrorMessage
();
$icon
=
$result
->
getIcon
();
if
(!
$icon
)
{
$icon
=
id
(
new
PHUIIconView
())
->
setIcon
(
'fa-commenting'
,
'green'
);
}
$control
=
id
(
new
PHUIFormTimerControl
())
->
setIcon
(
$icon
)
->
appendChild
(
$error
);
$status_challenge
=
$result
->
getStatusChallenge
();
if
(
$status_challenge
)
{
$id
=
$status_challenge
->
getID
();
$uri
=
"/auth/mfa/challenge/status/{$id}/"
;
$control
->
setUpdateURI
(
$uri
);
}
return
$control
;
}
/* -( Synchronizing New Factors )------------------------------------------ */
final
protected
function
loadMFASyncToken
(
PhabricatorAuthFactorProvider
$provider
,
AphrontRequest
$request
,
AphrontFormView
$form
,
PhabricatorUser
$user
)
{
// If the form included a synchronization key, load the corresponding
// token. The user must synchronize to a key we generated because this
// raises the barrier to theoretical attacks where an attacker might
// provide a known key for factors like TOTP.
// (We store and verify the hash of the key, not the key itself, to limit
// how useful the data in the table is to an attacker.)
$sync_type
=
PhabricatorAuthMFASyncTemporaryTokenType
::
TOKENTYPE
;
$sync_token
=
null
;
$sync_key
=
$request
->
getStr
(
$this
->
getMFASyncTokenFormKey
());
if
(
strlen
(
$sync_key
))
{
$sync_key_digest
=
PhabricatorHash
::
digestWithNamedKey
(
$sync_key
,
PhabricatorAuthMFASyncTemporaryTokenType
::
DIGEST_KEY
);
$sync_token
=
id
(
new
PhabricatorAuthTemporaryTokenQuery
())
->
setViewer
(
$user
)
->
withTokenResources
(
array
(
$user
->
getPHID
()))
->
withTokenTypes
(
array
(
$sync_type
))
->
withExpired
(
false
)
->
withTokenCodes
(
array
(
$sync_key_digest
))
->
executeOne
();
}
if
(!
$sync_token
)
{
// Don't generate a new sync token if there are too many outstanding
// tokens already. This is mostly relevant for push factors like SMS,
// where generating a token has the side effect of sending a user a
// message.
$outstanding_limit
=
10
;
$outstanding_tokens
=
id
(
new
PhabricatorAuthTemporaryTokenQuery
())
->
setViewer
(
$user
)
->
withTokenResources
(
array
(
$user
->
getPHID
()))
->
withTokenTypes
(
array
(
$sync_type
))
->
withExpired
(
false
)
->
execute
();
if
(
count
(
$outstanding_tokens
)
>
$outstanding_limit
)
{
throw
new
Exception
(
pht
(
'Your account has too many outstanding, incomplete MFA '
.
'synchronization attempts. Wait an hour and try again.'
));
}
$now
=
PhabricatorTime
::
getNow
();
$sync_key
=
Filesystem
::
readRandomCharacters
(
32
);
$sync_key_digest
=
PhabricatorHash
::
digestWithNamedKey
(
$sync_key
,
PhabricatorAuthMFASyncTemporaryTokenType
::
DIGEST_KEY
);
$sync_ttl
=
$this
->
getMFASyncTokenTTL
();
$sync_token
=
id
(
new
PhabricatorAuthTemporaryToken
())
->
setIsNewTemporaryToken
(
true
)
->
setTokenResource
(
$user
->
getPHID
())
->
setTokenType
(
$sync_type
)
->
setTokenCode
(
$sync_key_digest
)
->
setTokenExpires
(
$now
+
$sync_ttl
);
$properties
=
$this
->
newMFASyncTokenProperties
(
$provider
,
$user
);
if
(
$this
->
isAuthResult
(
$properties
))
{
return
$properties
;
}
foreach
(
$properties
as
$key
=>
$value
)
{
$sync_token
->
setTemporaryTokenProperty
(
$key
,
$value
);
}
$sync_token
->
save
();
}
$form
->
addHiddenInput
(
$this
->
getMFASyncTokenFormKey
(),
$sync_key
);
return
$sync_token
;
}
protected
function
newMFASyncTokenProperties
(
PhabricatorAuthFactorProvider
$provider
,
PhabricatorUser
$user
)
{
return
array
();
}
private
function
getMFASyncTokenFormKey
()
{
return
'sync.key'
;
}
private
function
getMFASyncTokenTTL
()
{
return
phutil_units
(
'1 hour in seconds'
);
}
final
protected
function
getChallengeForCurrentContext
(
PhabricatorAuthFactorConfig
$config
,
PhabricatorUser
$viewer
,
array
$challenges
)
{
$session_phid
=
$viewer
->
getSession
()->
getPHID
();
$engine
=
$config
->
getSessionEngine
();
$workflow_key
=
$engine
->
getWorkflowKey
();
foreach
(
$challenges
as
$challenge
)
{
if
(
$challenge
->
getSessionPHID
()
!==
$session_phid
)
{
continue
;
}
if
(
$challenge
->
getWorkflowKey
()
!==
$workflow_key
)
{
continue
;
}
if
(
$challenge
->
getIsCompleted
())
{
continue
;
}
if
(
$challenge
->
getIsReusedChallenge
())
{
continue
;
}
return
$challenge
;
}
return
null
;
}
/**
* @phutil-external-symbol class QRcode
*/
final
protected
function
newQRCode
(
$uri
)
{
$root
=
dirname
(
phutil_get_library_root
(
'phabricator'
));
require_once
$root
.
'/externals/phpqrcode/phpqrcode.php'
;
$lines
=
QRcode
::
text
(
$uri
);
$total_width
=
240
;
$cell_size
=
floor
(
$total_width
/
count
(
$lines
));
$rows
=
array
();
foreach
(
$lines
as
$line
)
{
$cells
=
array
();
for
(
$ii
=
0
;
$ii
<
strlen
(
$line
);
$ii
++)
{
if
(
$line
[
$ii
]
==
'1'
)
{
$color
=
'#000'
;
}
else
{
$color
=
'#fff'
;
}
$cells
[]
=
phutil_tag
(
'td'
,
array
(
'width'
=>
$cell_size
,
'height'
=>
$cell_size
,
'style'
=>
'background: '
.
$color
,
),
''
);
}
$rows
[]
=
phutil_tag
(
'tr'
,
array
(),
$cells
);
}
return
phutil_tag
(
'table'
,
array
(
'style'
=>
'margin: 24px auto;'
,
),
$rows
);
}
final
protected
function
getInstallDisplayName
()
{
$uri
=
PhabricatorEnv
::
getURI
(
'/'
);
$uri
=
new
PhutilURI
(
$uri
);
return
$uri
->
getDomain
();
}
final
protected
function
getChallengeResponseParameterName
(
PhabricatorAuthFactorConfig
$config
)
{
return
$this
->
getParameterName
(
$config
,
'mfa.response'
);
}
final
protected
function
getChallengeResponseFromRequest
(
PhabricatorAuthFactorConfig
$config
,
AphrontRequest
$request
)
{
$name
=
$this
->
getChallengeResponseParameterName
(
$config
);
$value
=
$request
->
getStr
(
$name
);
$value
=
(
string
)
$value
;
$value
=
trim
(
$value
);
return
$value
;
}
final
protected
function
hasCSRF
(
PhabricatorAuthFactorConfig
$config
)
{
$engine
=
$config
->
getSessionEngine
();
$request
=
$engine
->
getRequest
();
if
(!
$request
->
isHTTPPost
())
{
return
false
;
}
return
$request
->
validateCSRF
();
}
final
protected
function
loadConfigurationsForProvider
(
PhabricatorAuthFactorProvider
$provider
,
PhabricatorUser
$user
)
{
return
id
(
new
PhabricatorAuthFactorConfigQuery
())
->
setViewer
(
$user
)
->
withUserPHIDs
(
array
(
$user
->
getPHID
()))
->
withFactorProviderPHIDs
(
array
(
$provider
->
getPHID
()))
->
execute
();
}
final
protected
function
isAuthResult
(
$object
)
{
return
(
$object
instanceof
PhabricatorAuthFactorResult
);
}
}
Event Timeline
Log In to Comment