55 */
66namespace OC \Repair \Owncloud ;
77
8+ use OC \Authentication \Token \IProvider as ITokenProvider ;
89use OC \DB \Connection ;
910use OC \DB \SchemaWrapper ;
11+ use OCA \OAuth2 \Db \AccessToken ;
12+ use OCA \OAuth2 \Db \AccessTokenMapper ;
13+ use OCP \AppFramework \Utility \ITimeFactory ;
14+ use OCP \Authentication \Token \IToken ;
1015use OCP \DB \QueryBuilder \IQueryBuilder ;
1116use OCP \Migration \IOutput ;
1217use OCP \Migration \IRepairStep ;
18+ use OCP \Security \ICrypto ;
19+ use OCP \Security \ISecureRandom ;
1320
1421class MigrateOauthTables implements IRepairStep {
1522 /** @var Connection */
@@ -18,7 +25,14 @@ class MigrateOauthTables implements IRepairStep {
1825 /**
1926 * @param Connection $db
2027 */
21- public function __construct (Connection $ db ) {
28+ public function __construct (
29+ Connection $ db ,
30+ private AccessTokenMapper $ accessTokenMapper ,
31+ private ITokenProvider $ tokenProvider ,
32+ private ISecureRandom $ random ,
33+ private ITimeFactory $ timeFactory ,
34+ private ICrypto $ crypto ,
35+ ) {
2236 $ this ->db = $ db ;
2337 }
2438
@@ -36,26 +50,55 @@ public function run(IOutput $output) {
3650 return ;
3751 }
3852
39- $ output ->info ('Update the oauth2_access_tokens table schema. ' );
53+ // Create column and then migrate before handling unique index.
54+ // So that we can distinguish between legacy (from oc) and new rows (from nc).
4055 $ table = $ schema ->getTable ('oauth2_access_tokens ' );
4156 if (!$ table ->hasColumn ('hashed_code ' )) {
57+ $ output ->info ('Prepare the oauth2_access_tokens table schema. ' );
4258 $ table ->addColumn ('hashed_code ' , 'string ' , [
4359 'notnull ' => true ,
4460 'length ' => 128 ,
4561 ]);
62+
63+ // Regenerate schema after migrating to it
64+ $ this ->db ->migrateToSchema ($ schema ->getWrappedSchema ());
65+ $ schema = new SchemaWrapper ($ this ->db );
4666 }
67+
68+ $ output ->info ('Update the oauth2_access_tokens table schema. ' );
69+ $ table = $ schema ->getTable ('oauth2_access_tokens ' );
4770 if (!$ table ->hasColumn ('encrypted_token ' )) {
4871 $ table ->addColumn ('encrypted_token ' , 'string ' , [
4972 'notnull ' => true ,
5073 'length ' => 786 ,
5174 ]);
5275 }
5376 if (!$ table ->hasIndex ('oauth2_access_hash_idx ' )) {
77+ // Drop legacy access codes first to prevent integrity constraint violations
78+ $ qb = $ this ->db ->getQueryBuilder ();
79+ $ qb ->delete ('oauth2_access_tokens ' )
80+ ->where ($ qb ->expr ()->eq ('hashed_code ' , $ qb ->createNamedParameter ('' )));
81+ $ qb ->executeStatement ();
82+
5483 $ table ->addUniqueIndex (['hashed_code ' ], 'oauth2_access_hash_idx ' );
5584 }
5685 if (!$ table ->hasIndex ('oauth2_access_client_id_idx ' )) {
5786 $ table ->addIndex (['client_id ' ], 'oauth2_access_client_id_idx ' );
5887 }
88+ if (!$ table ->hasColumn ('token_id ' )) {
89+ $ table ->addColumn ('token_id ' , 'integer ' , [
90+ 'notnull ' => true ,
91+ ]);
92+ }
93+ if ($ table ->hasColumn ('expires ' )) {
94+ $ table ->dropColumn ('expires ' );
95+ }
96+ if ($ table ->hasColumn ('user_id ' )) {
97+ $ table ->dropColumn ('user_id ' );
98+ }
99+ if ($ table ->hasColumn ('token ' )) {
100+ $ table ->dropColumn ('token ' );
101+ }
59102
60103 $ output ->info ('Update the oauth2_clients table schema. ' );
61104 $ table = $ schema ->getTable ('oauth2_clients ' );
@@ -99,10 +142,10 @@ public function run(IOutput $output) {
99142 $ table ->addIndex (['client_identifier ' ], 'oauth2_client_id_idx ' );
100143 }
101144
102- $ this ->db ->migrateToSchema ($ schema ->getWrappedSchema ());
103-
104145 // Regenerate schema after migrating to it
146+ $ this ->db ->migrateToSchema ($ schema ->getWrappedSchema ());
105147 $ schema = new SchemaWrapper ($ this ->db );
148+
106149 if ($ schema ->getTable ('oauth2_clients ' )->hasColumn ('identifier ' )) {
107150 $ output ->info ("Move identifier column's data to the new client_identifier column. " );
108151 // 1. Fetch all [id, identifier] couple.
@@ -124,7 +167,10 @@ public function run(IOutput $output) {
124167 $ output ->info ('Drop the identifier column. ' );
125168 $ table = $ schema ->getTable ('oauth2_clients ' );
126169 $ table ->dropColumn ('identifier ' );
170+
171+ // Regenerate schema after migrating to it
127172 $ this ->db ->migrateToSchema ($ schema ->getWrappedSchema ());
173+ $ schema = new SchemaWrapper ($ this ->db );
128174 }
129175
130176 $ output ->info ('Delete clients (and their related access tokens) with the redirect_uri starting with oc:// or ending with * ' );
@@ -157,5 +203,50 @@ public function run(IOutput $output) {
157203 $ qbDeleteClients ->expr ()->iLike ('redirect_uri ' , $ qbDeleteClients ->createNamedParameter ('%* ' , IQueryBuilder::PARAM_STR ))
158204 );
159205 $ qbDeleteClients ->executeStatement ();
206+
207+ // Migrate legacy refresh tokens from oc
208+ if ($ schema ->hasTable ('oauth2_refresh_tokens ' )) {
209+ $ output ->info ('Migrate legacy oauth2 refresh tokens. ' );
210+
211+ $ qbSelect = $ this ->db ->getQueryBuilder ();
212+ $ qbSelect ->select ('* ' )
213+ ->from ('oauth2_refresh_tokens ' );
214+ $ result = $ qbSelect ->executeQuery ();
215+ $ now = $ this ->timeFactory ->now ()->getTimestamp ();
216+ $ index = 0 ;
217+ while ($ row = $ result ->fetch ()) {
218+ $ clientId = $ row ['client_id ' ];
219+ $ refreshToken = $ row ['token ' ];
220+
221+ // Insert expired token so that it can be rotated on the next refresh
222+ $ accessToken = $ this ->random ->generate (72 , ISecureRandom::CHAR_UPPER . ISecureRandom::CHAR_LOWER . ISecureRandom::CHAR_DIGITS );
223+ $ authToken = $ this ->tokenProvider ->generateToken (
224+ $ accessToken ,
225+ $ row ['user_id ' ],
226+ $ row ['user_id ' ],
227+ null ,
228+ "oc_migrated_client $ {clientId}_t {$ now }_i $ index " ,
229+ IToken::PERMANENT_TOKEN ,
230+ IToken::DO_NOT_REMEMBER ,
231+ );
232+ $ authToken ->setExpires ($ now - 3600 );
233+ $ this ->tokenProvider ->updateToken ($ authToken );
234+
235+ $ accessTokenEntity = new AccessToken ();
236+ $ accessTokenEntity ->setTokenId ($ authToken ->getId ());
237+ $ accessTokenEntity ->setClientId ($ clientId );
238+ $ accessTokenEntity ->setHashedCode (hash ('sha512 ' , $ refreshToken ));
239+ $ accessTokenEntity ->setEncryptedToken ($ this ->crypto ->encrypt ($ accessToken , $ refreshToken ));
240+ $ accessTokenEntity ->setCodeCreatedAt ($ now );
241+ $ accessTokenEntity ->setTokenCount (1 );
242+ $ this ->accessTokenMapper ->insert ($ accessTokenEntity );
243+
244+ $ index ++;
245+ }
246+ $ result ->closeCursor ();
247+
248+ $ schema ->dropTable ('oauth2_refresh_tokens ' );
249+ $ schema ->performDropTableCalls ();
250+ }
160251 }
161252}
0 commit comments